# Keystone documentation > The superpowered Node.js Headless CMS for developers. Build faster and scale further with the programmable open source GraphQL API back-end for structured content projects. # Migrate to @keystone-6/core@8.0.0 Source: https://keystonejs.com/docs/guides/migrate-to-8 `@keystone-6/core@8.0.0` and the related package versions introduce breaking changes across Keystone's configuration, Prisma, access control, auth and Admin UI. This guide explains how those changes affect an existing project and how to update. You can use this guide to upgrade from `@keystone-6/core@6` or `@keystone-6/core@7` because the breaking changes in `@keystone-6/core@7` were limited to upgrading to `next@15` and `react@19`. ## Update packages ### Runtime requirements Make sure you are using Node.js 20.19 or newer. ### Keystone packages ```bash pnpm add @keystone-6/core@^8 # only upgrade the packages that you are using pnpm add @keystone-6/auth@^10 pnpm add @keystone-6/fields-document@^11 pnpm add @keystone-6/cloudinary@^10 pnpm add @keystone-6/document-renderer@^2 ``` ### Required framework peers `@keystone-6/core@8.0.0` requires projects to declare `next`, `react` and `react-dom` as dependencies directly: ```sh pnpm add next@^16 react@^19 react-dom@^19 ``` ### Dependencies used directly by your project Several dependencies used internally by `@keystone-6/core@8.0.0` have also moved to new major or minimum versions. Only update these packages when you already declare them in your `package.json`. - `graphql@^16.14.2` - `express@^5` - `@apollo/client@^4` - `@apollo/server@^5` - `@graphql-ts/schema@^1` - `@graphql-ts/extend@^2` ### Prisma 7 Install both Prisma packages: ```sh pnpm add @prisma/client@^7.9 pnpm add --save-dev prisma@^7.9 ``` Then install the adapter/driver pair for each database provider the project builds or tests against: | Provider | Install | | ---------- | ------------------------------------------------------------------- | | SQLite | `pnpm add @prisma/adapter-better-sqlite3@^7.9 better-sqlite3@^12.6` | | PostgreSQL | `pnpm add @prisma/adapter-pg@^7.9 pg@^8.16` | | MySQL | `pnpm add @prisma/adapter-mariadb@^7.9 mariadb@^3.4` | ### Custom Admin UI dependencies Projects with custom Admin UI code must replace `@keystone-ui/*` with `@keystar/ui`. ```bash pnpm add @keystar/ui@0.9.2 ``` If you recieve a peer dependency warning, you should use the newer version ### Storage implementation dependencies Keystone no longer supplies named local or S3 storage strategies. If your existing configuration has a top-level `storage` property or an image/file field, install the libraries needed to implement that strategy in application code. For example, an S3 implementation can use: ```sh pnpm add @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner ``` A local filesystem implementation can use Node's built-in `node:fs` APIs and needs no storage package. ## Breaking changes ### Configure `prisma@7` #### Add the runtime adapter Replace Keystone's removed runtime connection options: ```ts // before db: { provider: 'postgresql', url: process.env.DATABASE_URL!, shadowDatabaseUrl: process.env.SHADOW_DATABASE_URL, enableLogging: true, } ``` with the adapter for the configured provider: ```ts import { PrismaPg } from '@prisma/adapter-pg' db: { provider: 'postgresql', prismaClientOptions: () => ({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL! }), log: ['query'], }), } ``` SQLite: ```ts import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: process.env.DATABASE_URL! }), }), } ``` MySQL: ```ts import { PrismaMariaDb } from '@prisma/adapter-mariadb' db: { provider: 'mysql', prismaClientOptions: () => ({ adapter: new PrismaMariaDb(process.env.DATABASE_URL!), }), } ``` #### Add Prisma CLI configuration Create or update `prisma.config.ts`: ```ts import 'dotenv/config' import { defineConfig, env } from 'prisma/config' export default defineConfig({ schema: 'schema.prisma', migrations: { path: 'migrations' }, datasource: { url: env('DATABASE_URL'), // only necessary if you want to use a specific shadow database shadowDatabaseUrl: env('SHADOW_DATABASE_URL'), }, }) ``` The runtime adapter and `prisma.config.ts` are separate: - Keystone uses `db.prismaClientOptions()`. - The `prisma` CLI uses `prisma.config.ts`. #### Update the generated client path Keystone now generates Prisma Client into `generated/prisma` by default: ```ts // before import { PrismaClient } from '@prisma/client' // after import { PrismaClient } from './generated/prisma/client' ``` The default generated Keystone types path has also changed to `generated/keystone/types`: ```ts // before import type { Lists } from '.keystone/types' // after import type { Lists } from './generated/keystone/types' ``` Update `.gitignore` to ignore the generated directory. #### Replace removed commands Update scripts: | Before | After | | ---------------------------------- | ---------------------------------------------------------------------- | | `keystone prisma ` | `prisma ` | | `keystone migrate ...` | an explicit Prisma migration command | | `keystone start --with-migrations` | a separate `prisma migrate deploy` release step, then `keystone start` | Remove `keystone postinstall --fix`, the `--fix` flag no longer exists. ### Update Keystone configuration #### Move field labels and descriptions - `field.label` becomes `field.ui.label` - `list.description` and `list.ui.description` are removed - a description intended for the GraphQL type becomes `list.graphql.description` #### Move default field modes Move list and field-group defaults: ```ts // before ui: { itemView: { defaultFieldMode: 'read' }, } // after fieldDefaults: { ui: { itemView: { fieldMode: 'read' }, }, } ``` Apply the same change for `createView` and `itemView`. #### Flatten `getAdditionalFiles` Replace an array of functions/nested arrays with one function that returns a flat array: ```ts // before ui: { getAdditionalFiles: [filesFromA, filesFromB], } // after ui: { getAdditionalFiles: async () => [ ...(await filesFromA()), ...(await filesFromB()), ], } ``` #### Handle count-mode relationships and nullable items For every relationship using `ui.displayMode: 'count'`, set: ```ts ui: { displayMode: 'count', itemView: { fieldMode: 'read' }, } ``` A dynamic mode may return only `'read'` or `'hidden'`. Custom field-view functions must also handle a nullable `item` argument in create or missing-item states. #### Move storage strategies onto fields If the old Keystone configuration has top-level named storage: ```ts export default config({ storage: { uploads: { kind: 's3', type: 'file', // provider options }, }, // ... }) attachment: file({ storage: 'uploads' }) ``` remove the top-level `storage` property and give each `file()` or `image()` field a strategy object: ```ts attachment: file({ storage: { async put(key, stream, meta, context) { // Upload the stream with the provider SDK. }, async delete(key, context) { // Delete the object with the provider SDK. }, url(key, context) { // Return a public or signed URL. }, }, }) ``` The new `StorageStrategy` consists directly of `put`, `delete` and `url`. It does not accept the old `kind`, `type`, bucket or local path configuration. Reuse a shared strategy object when several fields intentionally have identical behaviour. See the [local](https://github.com/keystonejs/keystone/tree/main/examples/assets-local) and [s3](https://github.com/keystonejs/keystone/tree/main/examples/assets-s3) examples for more details on how you could implement this. ### Update hooks and password fields #### Replace deprecated hooks Replace `validateInput` and `validateDelete` with operation-specific `validate` hooks: ```ts hooks: { validate: { create: createOrUpdateValidation, update: createOrUpdateValidation, delete: deleteValidation, }, } ``` #### Replace password options with a KDF Replace `bcrypt` and `workFactor`: ```ts import bcryptjs from 'bcryptjs' password({ kdf: { hash: secret => bcryptjs.hash(secret, 12), compare: (secret, hash) => bcryptjs.compare(secret, hash), }, }) ``` If neither old option was set, omit `kdf`; Keystone defaults to bcrypt with work factor 10. Existing bcrypt hashes remain compatible with that default. The default bcrypt KDF now rejects inputs longer than 72 UTF-8 bytes where previously they were silently truncated by bcrypt. ### Update access control #### Field `access.read` now applies to filtering and ordering In `@keystone-6/core@8.0.0`, a single field `access.read` function applies to item reads, filtering and ordering. Previously, field read access did not affect filtering or ordering, which were controlled separately by `isFilterable` and `isOrderable`. In most projects, the new behavior is the better default: if someone cannot read a field, they usually should not be able to infer information about it by filtering or ordering. When that matches what you want, keep the read function as is: ```ts access: { read: canReadItem, }, ``` | Old value | New `access` | New `graphql.omit` | | ---------------- | -------------------------------------------------- | -------------------------------------- | | absent or `true` | `allowAll` | not omitted | | `false` | `denyAll` | `graphql.omit.read.filter/order: true` | | function | the same function under `access.read.filter/order` | not omitted | For example, this preserves an explicit filtering rule and removes ordering from the GraphQL schema: ```ts // before text({ access: { read: canReadItem }, isFilterable: canFilter, isOrderable: false, }) // after text({ access: { read: { item: canReadItem, filter: canFilter, order: denyAll, }, }, graphql: { omit: { read: { item: false, filter: false, order: true, }, }, }, }) ``` #### Translate list defaults Move `defaultIsFilterable` and `defaultIsOrderable` into `fieldDefaults.access.read` and `fieldDefaults.graphql.omit.read` using the same mapping. #### Review `sudo` and internal scope `context.sudo()` bypasses access control and `graphql.omit`. `context.internal()` bypasses `graphql.omit` but retains access control. The `context.sudo().graphql.schema` shape now retains access-control instrumentation. Code that inspected a schema extension changes from: ```ts schema.extensions.sudo ``` to: ```ts schema.extensions.scope === 'internal' ``` ### Update auth configuration #### Remove unsupported auth options `@keystone-6/auth` removes: - `magicAuthLink`; - `passwordResetLink`; - `initFirstItem`; - the `/init` Admin UI page; and - the generated `createInitial*` mutation. See the [magic link example](https://github.com/keystonejs/keystone/tree/main/examples/auth-magic-link) for how you could implement magic link & password reset flows in your own project. #### Initial-user creation `db.onConnect` can seed development data to replace `initFirstItem`: ```ts db: { async onConnect(context) { const sudo = context.sudo() if (await sudo.db.User.count()) return // Create a development-only account with a random password }, } ``` This pattern is only suitable for development seeding. #### Review `endSession` `@keystone-6/core` no longer adds `endSession` when `context.session.end` exists. `@keystone-6/auth` adds it when required. If you're not using `@keystone-6/auth` and want to keep the `endSession` operation, add a schema extension for it. ### Update GraphQL and generated metadata #### Replace `graphql` with a context-bound `g` The `graphql` export from `@keystone-6/core` has been renamed to `g`. The old name is still exported as a deprecated alias, but new and migrated code should use `gWithContext` so resolvers receive the project's generated context type: ```ts import { gWithContext } from '@keystone-6/core' import type { Context } from './generated/keystone/types' const g = gWithContext() type g = gWithContext.infer ``` Rename runtime calls such as `graphql.object`, `graphql.field` and `graphql.arg` to `g.object`, `g.field` and `g.arg`. The type-level migration is not a direct namespace rename. Types such as `graphql.ObjectType` are replaced by the `g` inference helper applied to the corresponding builder: ```ts // before type Result = graphql.ObjectType // after type Result = g> ``` Use the same pattern for other functions where the old code referred to a type through the `graphql` namespace. See [graphql-ts.com](https://graphql-ts.com) for more details on this API. #### Preserve or accept list names Computed GraphQL plurals now preserve original casing. A new `list.graphql.singular` option can set singular output and operation names. After generating `schema.graphql`, compare it with the pre-upgrade version. For each changed public name either: - set `list.graphql.singular` or `list.graphql.plural` to preserve an existing API when preserving it is an explicit project rule; or - keep the new name and update usages to use the new name. #### Update enum names `select({ type: 'enum' })` and `multiselect({ type: 'enum' })` no longer singularise derived enum names. This affects fields with plural keys. For example: ```ts User: list({ fields: { roles: select({ type: 'enum', options: ['admin', 'editor'], }), }, }) ``` Previously, Keystone singularised the `roles` field key when generating its enum name: ```graphql enum UserRoleType { admin editor } ``` It now preserves the plural field key: ```graphql enum UserRolesType { admin editor } ``` The field key, column name and enum values are unchanged. Only the generated enum type identifier changes from `UserRoleType` to `UserRolesType`. Both field types use the new name in the GraphQL schema. Update checked-in GraphQL operations and generated types to the new name, or report the API decision. For `select({ type: 'enum' })`, the same name is used for the generated Prisma enum on PostgreSQL and MySQL: ```prisma // before enum UserRoleType { admin editor } // after enum UserRolesType { admin editor } ``` On PostgreSQL this also changes the name of a database enum type and requires a migration. MySQL uses the Prisma enum name when generating the client, but stores the enum values directly in the column definition without a named database type, so this change produces no database migration. SQLite continues to store an enum select in a string column. `multiselect({ type: 'enum' })` continues to use Prisma `Json` storage on every provider, so its enum-name change affects GraphQL only. #### Update Prisma error handling Direct `context.prisma` operations now throw Prisma errors instead of Keystone-wrapped `GraphQLError` values. Uncaught Prisma errors returned through GraphQL are masked with `code: 'KS_PRISMA_ERROR'` and `message: 'Prisma error'`. A custom formatter belongs under `graphql.apolloConfig.formatError`: ```ts graphql: { apolloConfig: { formatError(formattedError, error) { // Report the original error only to a trusted server-side service. return formattedError }, }, } ``` ### Update custom Admin UI code #### Replace `@keystone-ui/*` with `@keystar/ui` All usages of `@keystone-ui/*` should be replaced with `@keystar/ui`. The new package exports are not directly equivalent with the old one, so you will need to update imports and usage accordingly. See [keystar.design](https://keystar.design) for information on the new package and its usage. Note if you have a large amount of custom Admin UI code, this will likely be the most time-consuming part of the upgrade. Remove imports from `@keystone-6/fields-document/primitives`, those internals are no longer exported. ### Update test utilities Replace the generic import with the provider-specific helper: ```ts import { resetDatabase } from '@keystone-6/core/testing/sqlite' await resetDatabase({ filename: databasePath }, migrationsDirectory) ``` PostgreSQL uses `@keystone-6/core/testing/postgresql`; MySQL uses `@keystone-6/core/testing/mysql`. The new helpers destroy the target test database and apply `migration.sql` files. ## Database updates ### Review upstream `prisma@7` changes Compare direct Prisma usage with the [official `prisma@7` upgrade guide](https://www.prisma.io/docs/guides/upgrade-prisma-orm/v7). ### Migrations No database change is expected for most Keystone configuration migrations but you should review these specific cases: #### SQLite ##### JSON fields SQLite projects using `json`, `multiselect`, document, cloudinary or a custom `jsonFieldTypePolyfilledForSQLite` field change from Prisma `String` storage to Prisma `Json`. ##### Existing SQLite timestamps An existing SQLite database created through Prisma's former native driver may need: ```ts adapter: new PrismaBetterSqlite3( { url: process.env.DATABASE_URL! }, { timestampFormat: 'unixepoch-ms' } ) ``` #### PostgreSQL ##### Enum select names For a plural `select({ type: 'enum' })` field, Prisma may interpret the changed generated enum identifier as replacing both the enum and its column. For example, changing `UserRoleType` to `UserRolesType` can generate SQL equivalent to: ```sql CREATE TYPE "UserRolesType" AS ENUM ('admin', 'editor'); ALTER TABLE "User" DROP COLUMN "roles"; ALTER TABLE "User" ADD COLUMN "roles" "UserRolesType" NOT NULL; DROP TYPE "UserRoleType"; ``` When only the generated enum type name changed, replace the generated operations with a PostgreSQL enum rename: ```sql ALTER TYPE "UserRoleType" RENAME TO "UserRolesType"; ``` Adjust the identifiers to match the generated Prisma schema and verify that the enum values and column definition are otherwise unchanged. MySQL requires no database migration for this since MySQL does not use named enum types. {% related-content %} {% well heading="Database Migration" href="/docs/guides/database-migration" %} Create, review and deploy Prisma migration files after completing the source upgrade. {% /well %} {% well heading="System Configuration" href="/docs/config/config" %} Review the current Keystone configuration API. {% /well %} {% well heading="Auth and Access Control" href="/docs/guides/auth-and-access-control" %} Verify migrated access-control policy with a database. {% /well %} {% /related-content %} --- # Command Line Source: https://keystonejs.com/docs/guides/cli Keystone's command line interface (CLI) has been designed to help you develop, build, and deploy your Keystone project. Using the `keystone` command you can start the dev process, build and run your app in production. ### Usage ```text $ keystone [command] Commands dev start the project in development mode (default) postinstall build the project (for development, optional) build build the project (required by \`keystone start\`) start start the project telemetry sets telemetry preference (enable/disable/status) Options --frozen (build) don't build the graphql or prisma schemas, only validate them --no-db-push (dev) don't push any updates of your Prisma schema to your database --no-prisma (build, dev) don't build or validate the prisma schema --no-server (dev, start) don't start the express server --no-ui (build, dev, start) don't build and serve the AdminUI --quiet (dev, build, start) suppress most output except for errors ``` {% hint kind="tip" %} All the commands expect to find a module called `keystone.js` (or `.ts`) with a default export that returns a Keystone System `config()` from `@keystone-6/core`. See [System Configuration](../config/config) for details. {% /hint %} ## Setting up package.json We recommend adding the following scripts to your project's `package.json` file: ```json { "scripts": { "build": "keystone build", "dev": "keystone dev", "postinstall": "keystone postinstall", "migrate": "prisma migrate deploy", "start": "keystone start" } } ``` {% hint kind="tip" %} Note: Depending on where you are deploying the `prisma migrate deploy` step might be better in the `build` or as a separate step altogether. {% /hint %} Read on below for more details about each command, and see [bringing it all together](#bringing-it-all-together) for more details (including some important caveats) about how that "deploy" command works. ## dev ```bash $ keystone dev ``` This is the command you use to start Keystone for local development. It will: - Generate the files required by Keystone's APIs and Admin UI - Push database schema changes based on your Keystone schema - Start the local dev server, which hosts the GraphQL API and Admin UI ### dev flags - `--no-db-push` - Don't push any updates of your Prisma schema to your database - `--no-prisma` - Don't build or validate the prisma schema - `--no-server` - Don't start the express server, will still watch for changes and update the Admin UI and schema files - `--no-ui` - Don't build and serve the AdminUI ### About database migrations When using `keystone dev` the default behaviour is for Keystone to update your database to match your schema using Prisma `db push`. This behaviour is great for the rapid iteration of a schema, but can be modified in the following ways: - Running `keystone dev --no-db-push` - This will skip the dev migration step and not perform any checks on your database to ensure it matches your schema. This can be useful if you have an existing database or want to handle all migrations yourself. Be aware that this may lead to GraphQL runtime errors if a table or table column is unavailable. See [Using the Prisma CLI](#using-the-prisma-cli) below for more information on database migrations. {% hint kind="tip" %} Be careful of running `keystone dev` while pointing to a production database as this can cause data loss. {% /hint %} ### Resetting the database From time to time, in development you may want to reset the database and recreate it from scratch. You can do this by using Prisma: ```bash $ prisma db push --force-reset ``` {% hint kind="error" %} Doing this will destroy your database, including all data {% /hint %} This is typically useful early in a project's development lifecycle, when you want to test database initialisation scripts or create a fresh set of test data. ## postinstall ```bash $ keystone postinstall ``` {% hint kind="tip" %} Note: `postinstall` is an alias for `keystone build --no-ui --frozen` we recommend switching to this `build` command {% /hint %} Keystone generates several files that your app may depend on, including the Node.js API and TypeScript definitions. While they are generated by Keystone's `dev` command, we strongly recommend you add the `postinstall` command to your project's `package.json` to ensure the files are always present and up to date; otherwise you may see intermittent errors in your code editor and CI workflows. ### Fixing validation errors The `postinstall` command will warn you if any of the generated files you should have committed to source control (such as the Prisma and GraphQL Schemas) are out of sync with your Keystone Schema. The recommended way to fix this problem is to start your app using `keystone dev` (which will update these files) and commit any changes into source control, or use `keystone build --no-ui` to regenerate them. ## build ```bash $ keystone build ``` This command generates the files needed for Keystone to start in **production** mode. You should run it during the build phase of your production deployment. It will also validate that the generated files you should have committed to source control are in sync with your Keystone Schema. ### build flags - `--frozen` - Don't update the graphql or prisma schemas, only validate them, exits with error if the schemas don't match what keystone would generate. - `--no-prisma` - Don't build or validate the prisma schema - `--no-ui` - Don't build the AdminUI ## start ```bash $ keystone start ``` This command starts Keystone in **production** mode. It requires a build to have been generated (see `build` above). It will not generate or apply any database migrations - these should be run during the **build** or **release** phase of your production deployment. ### start flags - `--no-server` - Don't start the Express server - `--no-ui` - Don't serve the AdminUI ## Using the Prisma CLI ```bash $ prisma [command] ``` Use the [Prisma CLI](https://www.prisma.io/docs/reference/api-reference/command-reference/) directly within your Keystone project. Run `keystone build` or `keystone dev` first when Keystone's generated files, particularly the Prisma schema, need updating. Prisma reads its datasource and migration settings from your `prisma.config.ts`. ### Working with Migrations The primary reason you'll need to use the Prisma CLI in your Keystone project is to work with Prisma Migrate. For example, to deploy your migrations in production, you would run: ```bash $ prisma migrate deploy ``` This will run the Prisma [`migrate deploy`](https://www.prisma.io/docs/reference/api-reference/command-reference/#migrate-deploy) command. As you start working with more sophisticated migration scenarios, you'll probably also need to use the [`migrate resolve`](https://www.prisma.io/docs/reference/api-reference/command-reference/#migrate-resolve) and [`migrate status`](https://www.prisma.io/docs/reference/api-reference/command-reference/#migrate-status) commands. Keystone does not create or apply migration files itself. Familiarise yourself with the Prisma Migrate CLI commands when managing your application in production. ## Bringing it all together Although there are many subtle ways you might customise your workflow with Keystone based on your project's requirements, this is a fairly typical way of working: ### Postinstall after git clone and pull Setting up the `postinstall` script will make sure that Keystone's generated files are all present and correct when you get latest from source control and install dependencies. This will also run in CI, which means your lint rules and tests will be using the latest version of your Keystone build. ### Developing your project Use `keystone dev` to start Keystone for development. Commit changes to generated files, including the Prisma and GraphQL schema files. When using migrations, create and commit them separately with `prisma migrate dev`. ### Deploying to production Most application hosting platforms allow you to specify a `build` script (for new deployments) and a `start` script (for when the app is started, including new instances when scaling up). #### Build Script Install the project dependencies, generate the build and deploy migrations: ```bash npm i && npx keystone build && npx prisma migrate deploy ``` {% hint kind="error" %} Note: it is only safe to run migrations in the build step if deploys are built synchronously by your hosting provider. Make sure you don't run migrations against your production database from staging / preview builds. {% /hint %} #### Start Script Start Keystone in production mode: ```bash npx keystone start ``` Run `prisma migrate deploy` as a separate step before starting Keystone. `keystone start` does not apply migrations automatically. #### Notes - These examples use `npm` as the package manager; you can use others like `yarn` or `pnpm` if you prefer. - The commands above are included in the `package.json` reference at the top of this page. We recommend using package scripts so your build and start commands are centralised in source control. - If you promote your build through separate environments in a pipeline (e.g testing → staging → production) you should run migrations during the **promote** step and **not** as part of the build script. - It is important you do **not run migrations against your production database from staging builds**. If you have staging or preview environments set up in production, make sure they are not pointed to your production database. ## Related resources {% related-content %} {% well heading="Getting Started with create-keystone-app" grad="grad1" href="/docs/getting-started" target="" %} How to use Keystone's CLI app to standup a new local project with an Admin UI & the GraphQL Playground. {% /well %} {% /related-content %} --- # Understanding Relationships Source: https://keystonejs.com/docs/guides/relationships Relationships are the connections you make between different lists of content in Keystone. What you build depends a great deal on what you need. This guide will show you how to reason about, and configure relationships in Keystone so you can bring value to your project through structured content. --- ## How to reason about relationships Keystone provides a lot of flexibility when it comes to relationships. To get what you need there are two key questions you need to answer: 1. **Which side of the relationship do I need to access data from?** Your needs here will define whether your relationship needs to be [one, or two-sided](#one-sided-and-two-sided-relationships). 1. **How many connections do I need on either side of my relationship?** Understanding this will determine the kind of [cardinality](#establishing-cardinality) you need to configure. These topics are easier to understand by example. We’ll explore them, as well as the basics of setting up relationships through the use case of a **blog app** that has a `post` type for blog entries, and `user` type for post authors. Let’s get started… ## How to define a relationship in Keystone Relationships are made using the [`relationship`](../fields/relationship) field type within a [`list()`](../config/lists). In our blog example we can connect a blog `post` to some `users` using the relationship field’s `ref` configuration option like so: ```typescript{11} import { config, list } from '@keystone-6/core'; import { text, relationship } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text() } }), Post: list({ fields: { title: text(), content: text(), authors: relationship({ ref: 'User', many: true, }), }, }), }, }); ``` {% hint kind="tip" %} The `many` config option relates to *cardinality* which we explore later {% /hint %} ## One-sided & two-sided relationships In Keystone it’s possible to define relationships from one, or both sides of the two lists you’re connecting. We refer to these as *one-sided*, and *two-sided* relationships: ### One-sided Our example above is one-sided: the `Post` list relates to the `User` list via the `authors` field. This kind of relationship will let us query for the `authors` of a post in our GraphQL API like so: ```graphql query { posts { title content authors { name } } } ``` ### Two-sided If we can find all the `authors` of a post, we can also find all the posts written by a particular user. To do this we need to configure a two-sided relationship in our schema: ```typescript{9-10,17-18}[1-3] import { config, list } from '@keystone-6/core'; import { text, relationship } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text(), // relates posts to authors posts: relationship({ref: 'Post.authors', many: true }), }, }), Post: list({ fields: { title: text(), content: text(), // relates authors to posts authors: relationship({ ref: 'User.posts', many: true }), }, }), }, }); ``` In the example above we added a `posts` field to the `User` list, and changed the `ref` config of the `authors` field to be `User.posts`. {% hint kind="tip" %} In a two-sided relationship the `ref` config must be formatted as `.` and both sides must refer to each other. {% /hint %} Now that our relationship is two-sided we can query all the posts written by each user like so: ```graphql query { users { name posts { title content } } } ``` {% hint kind="warn" %} **Things to keep in mind:** Two-sided relationships are declared in two places, but **there is only one relationship between both lists**. **Both fields share the same data**. If you change the author of a post, that post will no longer show up in the original author’s posts. {% /hint %} ### Self-referencing relationships Keystone also lets you define one, and two-sided **relationships that refer to the same list**. To make a *one-sided* Twitter style following relationship we do the following: ```typescript{9}[1-3] import { config, list } from '@keystone-6/core'; import { text, relationship } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text(), follows: relationship({ ref: 'User', many: true }), }, }), }, }); ``` Or change this into a *two-sided* relationship to also access the followers of every user: ```typescript{9-10}[1-3] import { config, list } from '@keystone-6/core'; import { text, relationship } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text(), follows: relationship({ ref: 'User.followers', many: true }), followers: relationship({ ref: 'User.follows', many: true }), }, }), }, }); ``` {% hint kind="tip" %} The only relationship configuration not currently supported is having a field reference itself, e.g. `friends: relationship({ ref: 'User.friends', many: true })`. {% /hint %} ## Establishing cardinality *Cardinality* is a term used to describe how many items can exist on either side of a relationship. Each side can have either `one` or `many` related items. Since each relationship can have one and two sides, we have the following options available: {% table %} - Relationship type - One to one - One to many - Many to many --- - One-sided - {% emoji symbol="❌" alt="Not supported" /%} - {% emoji symbol="✅" alt="Supported" /%} - {% emoji symbol="✅" alt="Supported" /%} --- - Two-sided - {% emoji symbol="✅" alt="Supported" /%} - {% emoji symbol="✅" alt="Supported" /%} - {% emoji symbol="✅" alt="Supported" /%} {% /table %} Cardinality is defined through the `relationship` field’s `many` configuration option. It’s a boolean where: - `false` = one - `true` = many {% hint kind="tip" %} `false` is the default state for the many config option. {% /hint %} Let’s explore how to set up each type of cardinality in the context of our blog: ### One-sided cardinalities #### One-to-many {% #one-sided-one-to-many %} - Posts have a single author - Users can have multiple posts ```typescript{15}[1-3] import { config, list } from '@keystone-6/core'; import { text, relationship } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text(), }, }), Post: list({ fields: { title: text(), content: text(), author: relationship({ ref: 'User', many: false }), }, }), }, }); ``` #### Many-to-many {% #one-sided-many-to-many %} - Posts can have multiple authors - Users can have multiple posts ```typescript{15}[1-3] import { config, list } from '@keystone-6/core'; import { text, relationship } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text(), }, }), Post: list({ fields: { title: text(), content: text(), authors: relationship({ ref: 'User', many: true }), }, }), }, }); ``` ### Two-sided cardinalities {% hint kind="warn" %} In two-sided relationships the `many` option on both sides must be considered. {% /hint %} #### One-to-one - Posts have a single author - Users can create only one post ```typescript{9,16}[1-3] import { config, list } from '@keystone-6/core'; import { text, relationship } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text(), post: relationship({ ref: 'Post.author', many: false }), }, }), Post: list({ fields: { title: text(), content: text(), author: relationship({ ref: 'User.post', many: false }), }, }), }, }); ``` #### One-to-many {% #two-sided-one-to-many %} - Posts have a single author - Users can have multiple posts ```typescript{9,16}[1-3] import { config, list } from '@keystone-6/core'; import { text, relationship } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text(), posts: relationship({ref: 'Post.author', many: true }), }, }), Post: list({ fields: { title: text(), content: text(), author: relationship({ ref: 'User.posts', many: false }), }, }), }, }); ``` {% hint kind="tip" %} Note that we’ve used `many: false` in the author field and `many: true` in the posts field. {% /hint %} #### Many-to-many {% #two-sided-many-to-many %} - Posts can have multiple authors - Users can have multiple posts ```typescript{9,16}[1-3] import { config, list } from '@keystone-6/core'; import { text, relationship } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text(), posts: relationship({ ref: 'Post.authors', many: true }), }, }), Post: list({ fields: { title: text(), content: text(), authors: relationship({ ref: 'User.posts', many: true }), }, }), }, }); ``` {% hint kind="tip" %} Note that we have used `many: true` in both the authors and posts fields. {% /hint %} ## Summary Keystone relationships are managed using the [relationship](../fields/relationship) field type. They can be configured as one-sided or two-sided by the `ref` config option. Their cardinality can be set using the `many` flag. Keystone gives you the flexibility to choose what you want based on what you need to achieve. ## Related resources {% related-content %} {% well heading="Relationship Field API Reference" grad="grad1" href="/docs/fields/relationship" target="" %} Defines the names, types, and configuration of Keystone fields. See all the fields and the configuration options they accept. {% /well %} {% /related-content %} --- # Choosing the right Database Source: https://keystonejs.com/docs/guides/choosing-a-database Keystone supports [Postgres](https://www.postgresql.org), [MySQL](https://www.mysql.com) and [SQLite](https://www.sqlite.org/index.html) database [providers](/docs/config/config#db). This guide highlights the differences between these providers to help you choose the right one for your project. {% hint kind="tip" %} **Note:** SQLite is not recommended in production except for scenarios like the [Embedded Keystone](/blog/embedded-mode-with-sqlite-nextjs) example {% /hint %} ## Case Sensitivity By default, Postgres is case sensitive, while MySQL is not case sensitive, and SQLite is not case sensitive for `contains`, `startsWith` and `endsWith`. This means when filtering and sorting the results will differ depending on the database type and collation used. When using `StringFilter` in a GraphQL query the `mode` defaults to case sensitive for the database. You can make a query case insensitive in Postgres by using `mode: insensitive`. See the [Case Sensitivity Prisma Docs](https://www.prisma.io/docs/concepts/components/prisma-client/case-sensitivity) for more information. {% hint kind="tip" %} **Note:** `mode: insensitive` is not supported in MySQL or SQLite {% /hint %} ## Default Types Prisma has different default types for each database used, for example, the Keystone `text` field type uses the Prisma type of `String`, for Postgres, Prisma uses the `text` column type, and for MySQL, it uses the `varchar(191)` column type. For more details on the `field` type differences see [Prisma Schema Reference](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#model-field-scalar-types). The Keystone `text` field supports the `db.nativeType` option, allowing you to override this - [Fields API](/docs/fields/overview). Over time this option will be added to other field types. ## Auto Increment Integer Fields When using an [Integer field](/docs/fields/integer) with the `defaultValue: { kind: 'autoincrement' }` MySQL also requires this field to be indexed using the `isIndexed: true` or `isIndexed: 'unique'`. ## Supported Database Versions For supported database provider versions for Postgres and MySQL see [Prisma Supported Databases](https://www.prisma.io/docs/reference/database-reference/supported-databases) ## Related resources {% related-content %} {% well heading="Fields API" href="/docs/fields/overview" %} Documentation for the Fields API {% /well %} {% well heading="DB Config API" href="/docs/config/config#db" %} Documentation for the DB Config API {% /well %} {% /related-content %} --- # Database Migration Source: https://keystonejs.com/docs/guides/database-migration For rapid local development, `keystone dev` uses Prisma [`db push`](https://www.prisma.io/docs/orm/prisma-migrate/workflows/prototyping-your-schema) to make the connected database match Keystone's generated Prisma schema. For production and collaborative development, manage migration files by using the Prisma CLI directly. Keystone does not create or apply migrations. ## Prerequisites Configure the datasource and migrations directory in the `prisma.config.ts`: ```ts import { defineConfig } from 'prisma/config' export default defineConfig({ schema: 'schema.prisma', migrations: { path: 'migrations' }, datasource: { url: process.env.DATABASE_URL }, }) ``` Keystone creates this file when it doesn't exist, but doesn't update or read an existing file. Keystone uses its own `config.db.prismaSchemaPath` to define where the Prisma schema is written. When your provider requires a shadow database, configure it according to the [Prisma shadow database documentation](https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/shadow-database). ## Creating migrations in development First build the current Keystone schema, then ask Prisma to create and apply a development migration: ```sh keystone build --no-ui prisma migrate dev --name add-example ``` Prisma compares the generated schema, migration history, and development database. Review the generated `migrations/_add-example/migration.sql` and commit the migration directory with your schema changes. To create a migration without applying it immediately, use Prisma's `--create-only` option: ```sh prisma migrate dev --name add-example --create-only ``` Because `keystone dev` runs `db push`, it can make the development database drift from migration history. When actively developing migrations, either apply pending migrations before starting Keystone or disable automatic schema push: ```sh prisma migrate dev keystone dev --no-db-push ``` If the database does not match the generated schema, Prisma Client and GraphQL operations may fail because expected tables or columns are missing. ## Applying migrations in production Apply committed migrations with Prisma's non-interactive deployment command: ```sh prisma migrate deploy keystone start ``` Run `migrate deploy` as a distinct release or deployment step before starting new application instances. Do not run production migrations from preview or staging builds that point at a production database. For example: ```json { "scripts": { "migrate": "prisma migrate deploy", "start": "keystone start" } } ``` ## Inspecting and resolving migration state Use Prisma's other migration commands directly: ```sh prisma migrate status prisma migrate resolve --rolled-back "" ``` See the [Prisma Migrate documentation](https://www.prisma.io/docs/orm/prisma-migrate) for baselining existing databases, resolving failed migrations, generating down migrations, and handling migration conflicts. ## Resetting a development database Prisma can reset a development database and reapply its migration history: ```sh prisma migrate reset ``` For schema prototyping without migration files, you can instead force `db push` to recreate the database: ```sh prisma db push --force-reset ``` {% hint kind="error" %} Both reset workflows destroy data. Never run them against a production database. {% /hint %} ## Related Resources {% related-content %} {% well heading="Database Configuration" href="/docs/config/config#db" %} Configure Keystone's database provider and Prisma driver adapter. {% /well %} {% well heading="Keystone CLI" href="/docs/guides/cli#using-the-prisma-cli" %} Learn how Keystone's CLI fits alongside the Prisma CLI. {% /well %} {% /related-content %} --- # GraphQL Queries - Filters Source: https://keystonejs.com/docs/guides/filters Keystone provides a powerful GraphQL API for querying the data in your system. At the core of this API are **query filters**. This guide will show you how to use filters to get data you need. {% hint kind="tip" %} The filter references in this guide are based on the schema defined in the [Task Manager](https://github.com/keystonejs/keystone/tree/main/examples/usecase-todo) example project. {% /hint %} ## Scalar Filters If we want to find all the `Tasks` in our system, we can use the query `tasks()`. ```graphql { tasks { id label } } ``` In general we don't want to grab all the tasks at once, but we want to find a particular set of tasks which match a certain condition. In Keystone we can do this by passing a `where` argument to the `tasks()` query. If we want to find all the tasks with a label equal to `"Hello"` we can write: ```graphql { tasks(where: { label: { equals: "Hello" } }) { id label } } ``` Keystone provides a wide range of different filters. If we want to find all those tasks which **don't** have a label of `"Hello"` we can write: ```graphql { tasks(where: { label: { not: { equals: "Hello" } } }) { id label } } ``` The `text()` field type also supports searching for sub-strings within a field: ```graphql { tasks(where: { label: { contains: "He" } }) { id label } } ``` Different field types support different filters. The field `finishBy: timestamp()`, for example, lets you filter by tasks with a due date after a particular point in time: ```graphql { tasks(where: { finishBy: { gt: "2022-01-01T00:00:00.000Z" } }) { id label } } ``` For a full list of the different filters provided by each field type, please check the [Query Filter API](../graphql/filters). For more complex queries, you can combine multiple filters, and only those items which match all conditions will be returned. ```graphql { tasks(where: { label: { contains: "He" }, finishBy: { gt: "2022-01-01T00:00:00.000Z" } }) { id label } } ``` ## Combined Filters For more advanced queries, you might need to explicitly combine different filters. ### AND The `AND` operater accepts a list of sub-filters, and will only return those items which match all of these conditions. ```graphql { tasks(where: { AND: [ { label: { contains: "H" } }, { label: { contains: "ll" } } ] }) { id label } } ``` ### OR The `OR` operater accepts a list of sub-filters, and will only return those items which match at least one of these conditions. ```graphql { tasks(where: { OR: [ { label: { contains: "H" } }, { label: { contains: "ll" } } ] }) { id label } } ``` ### NOT The `NOT` operater accepts a list of sub-filters, and will only return those items which don't match the conditions. You'll generally only pass a single filter to `NOT` rather than a list but if you do a pass a list, they're `AND`ed together. ```graphql { tasks(where: { NOT: { label: { contains: "H" } } }) { id label } } ``` ## Relationship Filters As well as filtering by scalar fields, you can also filter against relationship fields. The Relationship filters available depend on the number of item that can exist on the far side of the relationship, that is, whether you have `many: true` or `many: false` [configured on the field](./relationships). ### To-One If you have `many: false` configured on the relationship field, then you can find items based on a `where` filter using the fields from the related list. For example, to find all the tasks where the task is assigned to a user named `"Alice"`, we can run the following query. ```graphql { tasks(where: { assignedTo: { name: { equals: "Alice" } } }) { id label } } ``` To find any tasks that have no assigned user, we can run the following query. ```graphql { tasks(where: { assignedTo: null }) { id label } } ``` Conversely, if we wanted tasks where any user was assigned, we can invert the above using the `NOT` operator: ```graphql { tasks(where: { NOT: { assignedTo: null } }) { id label } } ``` ### To-Many If you have `many: true` configured on the relationship field, then you can find items based on whether `some`, `none`, or `every` of the related items match a condition. The condition itself can be a `where` filter using the fields from the related list or an empty object (`{}`) to indicate all related items should be matched. #### Some The `some` filter returns true when at least one of the related items match the conditions supplied. For example, to find people that have one or more tasks where the task label contains `"shopping"`, we can run the following query: ```graphql { people(where: { tasks: { some: { label: { contains: "shopping" } } } }) { id name } } ``` To find people with any tasks at all, we could can pass an empty object. This indicate that no conditions should be applied to tasks so all tasks are matched: ```graphql { people(where: { tasks: { some: {} } }) { id name } } ``` #### None The `none` filter returns true when there are zero related items that match the conditions supplied. For example, if we had an urgent task we were looking to assign, we could query for people who didn't have any task that were both incomplete and high-priority: ```graphql { people(where: { tasks: { none: { priority: { equals: high }, isComplete: { equals: false } } } }) { id name } } ``` Or, if we wanted people with no assigned tasks at all (not even completed tasks), we could pass `none` as an empty object: ```graphql { people(where: { tasks: { none: {} } }) { id name } } ``` #### Every The `every` filter returns true when all related items match the conditions supplied. For example, if we wanted to find people who have completed their assigned tasks, we could write: ```graphql { people(where: { tasks: { every: { isComplete: { equals: true } } } }) { id name } } ``` Note the results will also include people with no tasks assigned at all. If you only wanted people who have both completed their assigned tasks _and_ actually have at least one task assigned, you could add a `some` filter: ```graphql { people(where: { tasks: { every: { isComplete: { equals: true } }, some: {} } }) { id name } } ``` Like the other "to-many" relationship filters, `every` does also accept an empty object, indicating no filters should be applied to the related items matched. However, doing so has no effect – it always evaluates to true, regardless of the properties of related items or whether they exist at all. So a query like this: ```graphql { people(where: { tasks: { every: {} } }) { id name } } ``` Could be re-written as simply: ```graphql { people { id name } } ``` ### Filtering Related Items In the relationship examples above we have been filtering items based on their related items, but we haven't been loading any information about the related items themselves. GraphQL gives us the ability to write queries that retrieve data across a graph of interconnected items - in Keystone, relationships form the links between these items. Queries that traverse these links apply filters both the top-level "list" fields (ie. `tasks` and `people` in these examples) and when selecting data about related items themselves. This is easier to explain with examples so let's expand on one from earlier. Lets say we want to find **people who have no high-priority, incomplete tasks** but we want to see **all the incomplete tasks they still have on their list**. We could write something like this: ```graphql { # Only return people who have no high-priority, incomplete tasks people(where: { tasks: { none: { priority: { equals: high }, isComplete: { equals: false } } } }) { id name # For each person, get tasks that are not complete tasks(where: { isComplete: { equals: false } }) { id label } } } ``` A good way to explore what's possible within your Keystone GraphQL API is using the [GraphQL Playground](https://github.com/graphql/graphql-playground) that should be [available on your GraphQL endpoint in dev](https://keystonejs.com/releases/2021-11-02#graph-ql-playground-and-apollo-sandbox) (ie. at [`http://localhost:3000/api/graphql`](http://localhost:3000/api/graphql)). ## Related resources {% related-content %} {% well heading="Query Filters API Reference" href="/docs/graphql/filters" %} The complete list of filters available to Keystone field types. {% /well %} {% /related-content %} --- # Hooks Source: https://keystonejs.com/docs/guides/hooks Keystone provides a powerful CRUD GraphQL API which lets you perform basic operations on your data. As your system evolves you'll find that you need to include business logic alongside these operations. In this guide we'll show you how to use `hooks` to enhance the core operations in different ways. For full details of the function signatures, please check out the [Hooks API](../config/hooks). ## What is a hook? A hook is a function you define as part of your [schema configuration](../config/lists) which is executed when a GraphQL operation is performed. Let's look at a basic example to log a message to the console whenever a new user is created. ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text(), email: text(), }, hooks: { afterOperation: ({ operation, item }) => { if (operation === 'create') { console.log(`New user created. Name: ${item.name}, Email: ${item.email}`); } } }, }), }, }); ``` This function will be triggered whenever we execute a `create`, `update`, or `delete` mutation in our GraphQL API. It will be executed once for each item being operated on. Because we only want to log when a user is created, we check the value of the `operation` argument. We then use the `item` argument to get the value of the newly created user. Now that we've got a sense of what a hook is, let's look at how we can use hooks to solve some common problems you'll hit when creating your system. ## Modifying incoming data When a `create` or `update` operation is called, you might want to apply some pre-processing to the data before saving it to your database. For example, if you have a blog post, you might want to ensure that the `title` field always starts with an upper-case letter. The `resolveInput` hook lets us take the data which has been provided to the GraphQL mutation and modify it before it is saved. Let's write a hook which takes the data for a blog post and converts the first letter to upper-case. ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { Post: list({ fields: { title: text({ validation: { isRequired: true } }), content: text({ validation: { isRequired: true } }), }, hooks: { resolveInput: ({ resolvedData }) => { const { title } = resolvedData; if (title) { return { ...resolvedData, // Ensure the first letter of the title is capitalised title: title[0].toUpperCase() + title.slice(1) } } // We always return resolvedData from the resolveInput hook return resolvedData; } }, }), }, }); ``` {% hint kind="tip" %} We must always return the modified `resolvedData` value from our hook, even if we didn't end up changing it. {% /hint %} The `resolveInput` hook is called whenever we update or create an item. The value of `resolvedData` will contain the input provided to the mutation itself, with any field type input resolvers applied. For example, a `password` field will convert the provided text input into an encrypted hash value. If you just want to see what the original input was, before the field type resolvers were applied, you can use the `inputData` argument. If you're performing an update operation, you might also want to access the current value of the item stored in the database. This is available as the `item` argument. Finally, all hooks are provided with a `context` argument, which gives you access to the full [context API](../context/overview). ## Validating inputs Before writing the resolved data to the database you will often want to check that it conforms to certain rules, depending on your application's needs. For example, you might want to ensure that blog posts do not have a blank title. An empty string, `""`, is a perfectly valid `String` value to pass into GraphQL. In the following example, we use a validation hook to check that this value is rejected when creating or updating a Post. ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { Post: list({ fields: { title: text({ validation: { isRequired: true } }), content: text({ validation: { isRequired: true } }), }, hooks: { validate: ({ resolvedData, addValidationError }) => { const { title } = resolvedData; if (title === '') { addValidationError('The title of a blog post cannot be the empty string'); } } }, }), }, }); ``` A `validate` hook is passed the `resolvedData` value after defaults and `resolveInput` hooks have been completed. We can check the values on this object and use the function `addValidationError` to return error messages. You use `addValidationError` multiple times to capture different problems. Keystone will abort the operation and convert these error messages into GraphQL errors which will be returned to the caller. The `validate` hook also receives the `operation`, `inputData`, `item` and `context` arguments if you want to perform more advanced checks. {% hint kind="warn" %} Don't confuse data **validation** with **access control**. If you want to check whether a user is **allowed** to do something, you should set up [access control rules](../config/access-control). {% /hint %} ## Triggering side-effects When data is changed in our system we might want to trigger some external side-effect. For example, we might want to send a welcome email to a user when they first create their account. We can use the `beforeOperation` and `afterOperation` hooks to do this. Let's send an email after a user is created. ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; // Keystone leaves it up to you to decide how best to implement email in your system import { sendWelcomeEmail } from './lib/welcomeEmail'; export default config({ lists: { User: list({ fields: { name: text(), email: text(), }, hooks: { afterOperation: ({ operation, item }) => { if (operation === 'create') { sendWelcomeEmail(item.name, item.email); } } }, }), }, }); ``` The `beforeOperation` and `afterOperation` hooks are very similar, but serve slightly different purposes. The `beforeOperation` hook receives an `item` argument, which contains the currently stored in the database before the operation. In the `afterOperation` hook, `item` represents the newly updated data in the database, and the original data from before the update is provided as `originalItem`. In a `create` operation, there won't be a pre-existing item, and for `delete` operations, the value of `item` in the `afterOperation` hook will be `null`. If the `beforeOperation` hook throws an exception then the operation will return an error, and the data will not be saved to the database. If the `afterOperation` hook throws an exception then the data will remain in the database. As such, `afterOperation` hooks should be used where a failure to execute isn't a critical problem. ## List hooks vs field hooks All of the examples above have involved hooks associated with a particular list. Keystone also supports setting of hooks associated with a particular field. All the same hooks are available, and they receive all the same arguments, along with an extra `fieldKey` argument. Field hooks can be useful if you want to have field specific rules. For example, you might have an email validation function which want to use in your system. You could always write this as a list hook, but it will make your code more clear if you write this as a field hook. ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text(), email: text({ validation: { isRequired: true }, hooks: { validate: ({ addValidationError, resolvedData, fieldKey }) => { const email = resolvedData[fieldKey]; if (email !== undefined && email !== null && !email.includes('@')) { addValidationError(`The email address ${email} provided for the field ${fieldKey} must contain an '@' character`); } }, }, }), }, }), }, }); ``` See the [Hooks API](../config/hooks) for the details of all the arguments available for all the different hook functions. ## Related resources {% related-content %} {% well heading="Hooks API Reference" href="/docs/config/hooks" %} The complete reference for executing code at different stages of the mutation lifecycle {% /well %} {% /related-content %} --- # Authentication and Access Control Source: https://keystonejs.com/docs/guides/auth-and-access-control Keystone comes with several features that work together to control access to the Admin UI and GraphQL API: - **Session Management** – a set of APIs for starting and ending user sessions, as well as initialising the session data for each request ([docs](../config/session)) - **The `auth` package** – an opinionated implementation of authentication features for Keystone apps ([docs](../config/auth)) - **Access Control** – a powerful framework for restricting access to specific lists, operations, and fields ([docs](../config/access-control)) - **Dynamic UI Config and Field Modes** – options for the Admin UI that work similarly to Access Control, and let you dynamically configure the Admin UI based on user permissions ([docs](../config/lists#ui)) Session Management and Auth are extremely flexible in Keystone, and it's possible to replace the default implementations we provide with your own (or integrate an entirely separate auth system), but in this guide we'll focus on how all these features are designed to work together. ## Setting up Users, Auth and Session While you can use Keystone's APIs to implement your own custom session and authentication systems, we've built one for you that probably does what you need most of the time: the `@keystone-6/auth` package. We recommend starting with that. Keystone is designed to make as few assumptions about your schema and system design as possible, which means it doesn't come with a built-in concept of **Users**. In your app, Users are just another List that you create and specify fields for – you can set them up however you like. ### Create a list for users To use the `auth` package, you need to nominate a list that stores your user accounts, and that lists needs at least two fields: an **identity** field (e.g username or email address, it should be unique) that users are looked up by when they sign in; and a **secret** field (i.e password) that is used to verify them. You can add any other fields you want to your users list, including contact information, permissions, roles, and relationships to other entities in your database. Here's an example: ```ts const Person = list({ access: allowAll, fields: { name: text(), email: text({ isIndexed: 'unique' }), password: password(), isAdmin: checkbox(), }, }) ``` {% hint kind="tip" %} Read more about creating lists in the [schema](../config/lists) and [fields](../fields/overview) API Docs. {% /hint %} ### Configure authentication With our users list in place, we can start configuring our authentication: ```ts import { createAuth } from '@keystone-6/auth' const { withAuth } = createAuth({ listKey: 'Person', identityField: 'email', secretField: 'password', }) ``` The `createAuth` function returns another function called `withAuth` that will automatically extend Keystone's config to set up everything we need. Behind the scenes, the `auth` package is just using lower-level Keystone APIs to do everything, which means if you want to do something differently, you can fork our implementation of `auth` and build your own (this is true for session management as well) {% hint kind="tip" %} Read more about `createAuth` in the [Auth API Docs](../config/auth). {% /hint %} ### Configure sessions Finally we need to tell Keystone how to track sessions. The simplest method is to use **stateless sessions**, which use an encrypted cookie to store enough information for Keystone to identify the item on each request. They're like JWTs, but without the downsides. ```ts const session = statelessSessions({ secret: '-- EXAMPLE COOKIE SECRET; CHANGE ME --', }) ``` You can use your own session strategy if for example, if you want to use use OAuth sessions. {% hint kind="tip" %} Read more about [Session Stores in the Session API Docs](../config/session#session-stores). {% /hint %} ### Putting it all together Your Keystone config should now look like this: ```ts import { config, list } from '@keystone-6/core' import { checkbox, password, text } from '@keystone-6/core/fields' import { statelessSessions } from '@keystone-6/core/session' import { createAuth } from '@keystone-6/auth' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' const db = { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: process.env.DATABASE_URL || 'file:./keystone-example.db', }), }), } const { withAuth } = createAuth({ listKey: 'Person', identityField: 'email', secretField: 'password', }) const session = statelessSessions({ secret: '-- EXAMPLE COOKIE SECRET; CHANGE ME --', }) const lists = { Person: list({ access: allowAll, fields: { name: text(), email: text({ isIndexed: 'unique' }), password: password(), isAdmin: checkbox(), }, }), } export default withAuth( config({ db, lists, session, }) ) ``` ## Loading Session Data At the start of every request, Keystone will initialise in-memory session data for the request. When you're using the `auth` package, a session cookie is used to identify an item in the database that represents the current user, and then a query is run to load data about that item. The result of the query is stored in `context.session`, and available to almost every function and hook in Keystone – including access control. In this initialisation phase, you'll want to load any data you'll need to work out what the user should be able to do (and what they should not be able to do). If no session cookie is present, or no matching item can be found, `context.session` will be `undefined`. ### Add a sessionData query In our example above, we probably want to know the current user's ID and the value of the `isAdmin` checkbox. The `auth` package makes this simple by providing a `sessionData` option, which should contain the fields to query when a session is found. You configure it like this: ```ts const { withAuth } = createAuth({ // ... sessionData: 'isAdmin', }) ``` Think of this like the field selection in a GraphQL query. You can load any fields you need to have at hand when writing Access Control methods, including virtual fields and relationships. The equivalent GraphQL query would look like this: ```graphql query { person(where: { id: $session.itemId }) { id isAdmin } } ``` ## Adding Access Control Now that we have information about the current user, lets put it to good use and add some security to our API with **Access Control**. The way to think about access control is – - With **Session Data**, you know who the user is, and what they should have access to (whether that's permissions granted by a role, or items related to them) - For each **List** you can restrict which operations they can perform, filter the items they have access to, and control which fields they can read and update. {% hint kind="tip" %} In other words: **Step 1** load data about the current user; **Step 2** use that data to limit what they can do. {% /hint %} You define access control for each List (and field) individually; the rules you specify will always be run, even when the list is being queried or mutated through a relationship. ### Posts Example Let's say we have a list of Blog Posts, which have a published status. We want Admin users to see all blog posts and be able to create and update them, but anonymous (public) users can only see published posts and can't write to the API. Our Post list might look like this: ```ts const Post = list({ fields: { title: text(), isPublished: checkbox(), publishDate: timestamp(), author: relationship({ ref: 'Person' }), // more content fields would go here }, }) ``` And the Session Data we set up above would look like this: ```ts type Session = { data: { id: string isAdmin: boolean } } ``` We can now set up **operation** access control to restrict the **create**, **update** and **delete** operations to authenticated users with the `isAdmin` checkbox set: ```ts const isAdmin = ({ session }: { session?: Session }) => Boolean(session?.data.isAdmin) const Post = list({ access: { operation: { query: isAdmin, create: isAdmin, update: isAdmin, delete: isAdmin, }, }, fields: { // see above }, }) ``` We can also use **filter** access control to make sure that unauthenticated users can only see published posts: ```ts function filterPosts({ session }: { session?: Session }) { // if the user is an Admin, they can access all the records if (session?.data.isAdmin) return true // otherwise, filter for published posts return { isPublished: { equals: true } } } const Post = list({ access: { operation: { // see above }, filter: { query: filterPosts, }, }, fields: { // see above }, }) ``` When there's no session, or the user is not an Admin, the `filterPosts` function returns a filter object. This is the same format as the `where` arguments you can provide to the `posts` query through the GraphQL API: ```graphql query { posts(where: { isPublished: { equals: true } }) { title } } ``` {% hint kind="tip" %} An easy way to set up Access Control filters is to write them as queries in the GraphQL Playground, then test them to make sure they return the results you expect. When you're happy, copy and paste the `where` input into your access control function. {% /hint %} These filters are **combined** with any filters provided through the GraphQL API, and there's no way to circumvent them when making HTTP Requests. You can also return a boolean value to switch on or off access to a list entirely: - `true` allows access to all items in the list - `false` prevents access to all items in the list In the example above, if the user has `isAdmin` we return `true` allowing them to access all posts. Access control is always applied when querying or mutating items in a list, regardless of how GraphQL queries are written. For example, if we also had a **Tags** list that relates to **Posts**: ```ts const Tag = list({ fields: { label: text(), posts: relationship({ ref: 'Post', many: true }), }, }) ``` You can query all the posts linked to each tag, but the filters we've defined above on `Post` will still be applied. ```graphql query { tags { posts { title } } } ``` Unauthenticated users won't be able to find unpublished posts by querying the relationship field on tags, and you don't have to do anything special to make this work. ### Operations Each specific operation that can be performed through the GraphQL API has corresponding access control you can provide. The four operations are: - Query - Create - Update - Delete Access control is operation-specific, so if you want to prevent _any_ changes being made to items in a list, you would need to provide settings for all the mutation operations: `create`, `update` and `delete`. #### Why it's `query` and not `read` Because of the way GraphQL works, you can retrieve an item through both **queries** _and_ **mutations**. The `query` rules don't affect mutations, so our **list** access control is aligned to GraphQL operations. For example, both the `person` query and `updatePerson` mutation will find a Person by their ID and return their name: ```graphql query { person(where: { id: 1 }) { name } } mutation { updatePerson(where: { id: 1 }) { name } } ``` {% hint kind="tip" %} Note: this is different to **field** access control, where the `read` access control rule will affect a user's ability to retrieve the value for that field regardless of the query or mutation. {% /hint %} ### Different levels of Access Control Keystone provides three different levels of access control for lists, as well as field-level access control. Here are the available functions for lists: ```ts type Filter = Record // the GraphQL Filters for the List type Access = { operation: { query: ({ session, context, listKey, operation }) => boolean create: ({ session, context, listKey, operation }) => boolean update: ({ session, context, listKey, operation }) => boolean delete: ({ session, context, listKey, operation }) => boolean } filter: { query: ({ session, context, listKey, operation }) => Filter | boolean update: ({ session, context, listKey, operation }) => Filter | boolean delete: ({ session, context, listKey, operation }) => Filter | boolean } item: { create: ({ session, context, listKey, operation, inputData }) => boolean update: ({ session, context, listKey, operation, inputData, item }) => boolean delete: ({ session, context, listKey, operation, item }) => boolean } } ``` {% hint kind="tip" %} We'll cover field access control separately below. {% /hint %} You'll generally only need to use a subset of the available options: - If you want to block an operation based on Session Data, use `operation` - If you want to filter items based on Session Data, use `filter` - If you need to block an operation **or** filter items based on Session Data, use `filter` - If you need to check some logic based on the item data or input data, use `item` #### How `filter` access control works Filter access control is combined with `where` input from the GraphQL Query to create the full set of conditions provided to the database query. It can be used to limit which records can be queried and mutated in your system. In addition to returning filters, you can return a boolean where: - `true` effectively means match _all_ records (no filter is applied) - `false` effectively means match _no_ records (the operation will be blocked) {% hint kind="tip" %} Returning a boolean from filter access control is functionally the same as using operation access control, so if you specify filter access control, you rarely also need to specify operation access control. {% /hint %} Query filters are also applied to count operations, so you can paginate items predictably without revealing the existence of items the user does not have access to. Although Keystone doesn't implement database-level security, the design and effect of `filter` access control is similar to [Row Level Security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) in Postgres, with the benefit of being expressed declaratively in TypeScript and using the same filter syntax as the GraphQL API. This makes it easier to reason about in the context of your schema, session data, and other logic in your application. You can't specify filters for the `create` operation because you can't query records that don't exist yet. #### How `item` access control works When mutating items, item access control is checked against the mutation input (for `create` and `update`) and the item that has been retrieved from the database (for `update` and `delete`). This gives you the most granular control over whether an operation is allowed to proceed, but is the least performant because: - Keystone retrieves all matching records from the database before the access control rule can be evaluated - For the operations `updateMany` and `deleteMany` item access control function is invoked once for each item being updated or deleted If you need to limit access to some items in a list, filter access control is usually the best option. Item access control should only be used if you have logic that can't be expressed as a filter, or if you need to check the `inputData` provided to the mutation. ### Access Control in Hooks and Extensions Because the current request (including Session Data) is bound to the **Keystone Context**, any queries or mutations you run inside Hooks, Virtual Fields, and GraphQL Extensions will respect access control. For example, if we have a custom query that returns all posts written in the last week, sorted by publish date: ```ts const extendGraphqlSchema = graphQLSchemaExtension({ typeDefs: ` type Query { recentPosts: [Post!] }`, resolvers: { Query: { recentPosts: (root, args, context) => { var oneWeekAgo = new Date() oneWeekAgo.setDate(oneWeekAgo.getDate() - 7) return context.db.Post.findMany({ where: { publishDate: { gt: oneWeekAgo.toUTCString() } }, orderBy: { publishDate: 'desc' }, }) }, }, }, }) ``` The query will respect the access control rules for the `Post` list, and unauthenticated users will only see published posts. ### Circumventing Access Control Sometimes, you need to bypass access control. For example, you may have a hook that needs to query or mutate data the user making the request doesn't have access to; or you might have blocked access to creating new User items and instead written a custom signup mutation. When you need it, you can call `context.sudo()` to create a new context with elevated privileges that will bypass access control. Combining this with GraphQL extensions is a great way to keep your access control rules relatively simple, while carefully exposing specific functionality through your API. For example, we probably want to block all public access to querying users in our system: ```ts const isAdmin = ({ session }: { session?: Session }) => Boolean(session?.data.isAdmin) const Person = list({ access: { query: isAdmin, create: isAdmin, update: isAdmin, delete: isAdmin, }, fields: { // see above }, }) ``` But if we have a signup form in our app, we may want to be able to check whether an email address is in use so we can do client-side validation, without giving away any other information. We can create a custom query for this, and use `context.sudo()` to bypass access control and find a matching user, while only returning a boolean from the query: ```ts const extendGraphqlSchema = graphQLSchemaExtension({ typeDefs: ` type Query { isEmailInUse(email: String!): Boolean! }`, resolvers: { Query: { isEmailInUse: async (root, { email }, context) => { const sudoContext = context.sudo() const emailCount = await sudoContext.db.User.count({ where: { email: { equals: email, mode: 'insensitive' }, }, }) return !!emailCount }, }, }, }) ``` ## Field Access Control We've covered access control for lists now; but Keystone also lets you specify field-level access control, which is important when users should only be able to perform operations on _some of the fields_ in items that they can access. You can provide field-level rules for: - Read – applied when the field is selected through any GraphQL operation - Create – applied when items are being created - Update – applied when items are being updated {% hint kind="tip" %} If you want to completely block users from setting a field's value, make sure you set both the `create` and `update` rules. {% /hint %} {% hint kind="warn" %} `read` field access control does not apply to `context.db.*` operations, as these operations do not resolve the underlying fields using GraphQL. {% /hint %} For more information about the arguments provided to field rules, see the [Access Control API Docs](../config/access-control#field-access-control) ### People Example Building on our blog example above, let's implement the following rules for our **Person** list: - Anyone should be able to see the names of people in the system (otherwise we wouldn't be able to display authors' names) - Visitors are never able to see a person's email addresses - Users are able to see their own email address - Users can update their own name and email address - Users can only update their own password - Admins can update information for any user _except_ their password - Only admins can change the `isAdmin` checkbox - Only admins can create and delete users The implementation of these rules would look like this: ```ts type PersonData = { id: string name: string email: string isAdmin: boolean } // Validate there is a user with a valid session const isUser = ({ session }: { session?: Session }) => !!session?.data.id // Validate the current user is an Admin const isAdmin = ({ session }: { session?: Session }) => Boolean(session?.data.isAdmin) // Validate the current user is updating themselves const isPerson = ({ session, item }: { session?: Session; item: PersonData }) => session?.data.id === item.id // Validate the current user is an Admin, or updating themselves const isAdminOrPerson = ({ session, item }: { session?: Session; item: PersonData }) => isAdmin({ session }) || isPerson({ session, item }) const Person = list({ access: { operation: { query: isAdmin, create: isAdmin, update: isAdmin, delete: isAdmin, }, item: { update: isAdminOrPerson, }, }, fields: { name: text(), email: text({ isIndexed: 'unique', access: { read: isAdminOrPerson, }, }), password: password({ access: { // Note: password fields never reveal their value, only whether a value exists read: isAdminOrPerson, update: isPerson, }, }), isAdmin: checkbox({ access: { read: isUser, update: isAdmin, }, }), }, }) ``` ## Related resources {% related-content %} {% well heading="Authentication" href="/docs/config/auth" %} Documentation for the Auth Package API {% /well %} {% well heading="Access Control" href="/docs/config/access-control" %} Documentation for the Access Control API {% /well %} {% /related-content %} --- # Images and Files Source: https://keystonejs.com/docs/guides/images-and-files Keystone [fields](../fields/overview) include the `image` and `file` types. You can use them to manage images and/or files from within Keystone. This guide will show you how to configure images and files in your Keystone schema backed with [Amazon S3 Storage](https://aws.amazon.com/s3/), and thereby guide you on how you could setup a similar setup with a different storage provider. ## Storage with Amazon S3 Image and file fields accept a `storage` object that can define the functions for how assets will be uploaded, deleted and linked when interacting with Keystone's GraphQL API. This example shows an example of how you could store public images in a public S3 bucket, and private files in a private S3 bucket. ```typescript import { list } from '@keystone-6/core' import { allowAll } from '@keystone-6/core/access' import { text, image, file } from '@keystone-6/core/fields' import { getSignedUrl } from '@aws-sdk/s3-request-presigner' import { S3, GetObjectCommand } from '@aws-sdk/client-s3' import { Upload } from '@aws-sdk/lib-storage' const s3 = new S3({ /* configure the s3 client however you would like */ }) const publicBucketName = 'my-public-bucket' const privateBucketName = 'my-private-bucket' export const lists = { Post: list({ access: allowAll, fields: { title: text({ validation: { isRequired: true } }), content: text(), banner: image({ storage: { async put(key, stream, meta) { const upload = new Upload({ client: s3, params: { Bucket: publicBucketName, Key: key, Body: stream, ContentType: meta.contentType }, }) await upload.done() }, async delete(key) { await s3.deleteObject({ Bucket: publicBucketName, Key: key }) }, url(key) { return `https://${publicBucketName}.s3.amazonaws.com/${key}` }, }, }), attachment: file({ storage: { async put(key, stream, meta) { const upload = new Upload({ client: s3, params: { Bucket: privateBucketName, Key: key, Body: stream, ContentType: meta.contentType, }, }) await upload.done() }, async delete(key) { await s3.deleteObject({ Bucket: privateBucketName, Key: key }) }, url(key) { return getSignedUrl(s3, new GetObjectCommand({ Bucket: privateBucketName, Key: key })) }, }, }), }, }), } ``` ## How Content-Type differs between Images & Files - For **files**, Keystone always provides `meta.contentType as `application/octet-stream`. - For **images**, the `Content-Type` is set based on the image format that Keystone detects from the bytes of the image. ## Related Resources {% related-content %} {% well heading="Config API Reference" href="/docs/config/config" %} The complete reference for the base keystone configuration {% /well %} {% well heading="Fields API Reference" href="/docs/fields/overview" %} The complete reference for Field configuration {% /well %} {% well heading="Relationship Guide" href="/docs/fields/relationship" %} Learn how to reason about and configure relationships in Keystone so you can bring value to your project through structured content. {% /well %} {% well heading="Example Project: S3 Assets" href="https://github.com/keystonejs/keystone/tree/main/examples/assets-s3" %} A full keystone project illustrating how to configure `storage` with S3. {% /well %} {% well heading="Example Project: Local Assets" href="https://github.com/keystonejs/keystone/tree/main/examples/assets-local" %} A full keystone project illustrating how to configure `storage` using the local file system. {% /well %} {% /related-content %} --- # GraphQL Schema Extension Source: https://keystonejs.com/docs/guides/schema-extension Keystone automatically generates a GraphQL schema based on your [Keystone config](../config/config). This schema contains all GraphQL types, queries and mutations based on your lists and ends up as the generated `schema.graphql` file found in the root of your Keystone project. Generally changing the behavior of Keystone can be performed through [Hooks](../config/hooks), however, there are times when you need an extra GraphQL type or want a custom mutation or query, for these instances, Keystone has the `extendGraphqlSchema` option. The `extendGraphqlSchema` option expects a function that takes the GraphQL Schema generated by Keystone and returns a valid GraphQL schema. You can then use this function to add or replace resolvers and types. ## Using Keystone's g.extend Keystone exports `g` from `@keystone-6/core`, this uses [@graphql-ts/schema](https://docsmill.dev/npm/@graphql-ts/schema) which can be used in combination with `Context` from `generated/keystone/types` to extend your GraphQL schema in a type-safe way. The `g` export is pre-bound to Keystone's `Context` type. If you need to bind `g` to a custom context type, you can use `gWithContext` from `@keystone-6/core` instead. You can then import this into your Keystone configuration file ```ts import { g } from '@keystone-6/core'; ``` Then you can use `g.extend` to add custom resolvers to your GraphQL Schema. The following example adds a custom mutation called `publishPost` to the `base` Keystone Schema. It accepts one argument (`args`) of `id` which cannot be null and has the type of ID (`g.nonNull(g.ID)`). It updates the Post with the corresponding `id` and sets its `status` to 'published' and `publishDate` to the current time. It then returns the Post that is updated which has a GraphQL type of `Post` that is passed in from the `base` schema (`base.object('Post')`). ```ts import { g, config } from '@keystone-6/core'; import { Context } from './generated/keystone/types'; export default config({ {/* ... */}, graphql: { extendGraphqlSchema: g.extend(base => { return { mutation: { publishPost: g.field({ type: base.object('Post'), args: { id: g.arg({ type: g.nonNull(g.ID) }) }, resolve(source, { id }, context:Context) { return context.db.Post.updateOne({ where: { id }, data: { status: 'published', publishDate: new Date().toISOString() }, }); }, }), }, }; }), } }); ``` {% hint kind="tip" %} Note `context.db` is used in the resolver, this ensures the correct Internal object with the correct type is returned to GraphQL. {% /hint %} A full example project using `graphql-ts` can be found in [examples/extend-graphql-schema-graphql-ts](https://github.com/keystonejs/keystone/tree/main/examples/extend-graphql-schema-graphql-ts) on the Keystone GitHub repo. ## Using Third-Party Tools As `extendGraphqlSchema` expects a function that returns a valid GraphQL schema you can also use third-party GraphQL schema tools to help generate or merge schemas. ### GraphQL-Tools Merge Schemas [GraphQL Tools](https://www.graphql-tools.com/) `mergeSchemas` is a third-party package that can help with schema merging and adding custom resolvers and types, and then return an updated GraphQL schema to Keystone. Start by installing `@graphql-tools/schema` ```bash npm install @graphql-tools/schema ``` Then import into your Keystone configuration ```ts import { mergeSchemas } from '@graphql-tools/schema'; ``` You can then write custom `resolvers` and `typeDefs` to merge with your Keystone `schema`. For example, to add the same custom mutation as above (`publishPost`), you would add the `typeDefs` for `publishPost` and then the `resolvers.Mutation.publishPost` which performs the same function as above. ```ts export default config({ {/* ... */}, graphql: { extendGraphqlSchema: schema => mergeSchemas({ schemas: [schema], typeDefs: ` type Mutation { publishPost(id: ID!): Post `, resolvers: { Mutation: { publishPost: (root, { id }, context) => { return context.db.Post.updateOne({ where: { id }, data: { status: 'published', publishDate: new Date().toUTCString() }, }); }, }, }, }), } }); ``` {% hint kind="tip" %} Note - Before version `3.0.0` of `@keystone-6/core`, `@graphql-tools/schema` was exported from `@keystone/core` as `graphQLSchemaExtension` this was removed in favor of using the tool directly if required {% /hint %} A full example project using `@graphql-tools/schema` can be found in [examples/extend-graphql-schema-graphql-tools](https://github.com/keystonejs/keystone/tree/main/examples/extend-graphql-schema-graphql-tools) on the Keystone GitHub repo. ## Related resources {% related-content %} {% well heading="Config API Reference" href="/docs/config/config" %} The complete reference for the base keystone configuration {% /well %} {% well heading="Example Project: Extend GraphQL Schema with graphql-ts" href="https://github.com/keystonejs/keystone/tree/main/examples/extend-graphql-schema-graphql-ts" %} A full keystone project illustrating how to extend your GraphQL schema using graphql-ts provided by Keystone. {% /well %} {% well heading="Example Project: Extend GraphQL Schema with GraphQL-Tools" href="https://github.com/keystonejs/keystone/tree/main/examples/extend-graphql-schema-graphql-tools" %} A full keystone project illustrating how to extend your GraphQL schema using @graphql-tools/schema. {% /well %} {% well heading="Example Project: Extend GraphQL Schema with Nexus" href="https://github.com/keystonejs/keystone/tree/main/examples/extend-graphql-schema-nexus" %} A full keystone project illustrating how to extend your GraphQL schema using Nexus and @graphql-tools/schema. {% /well %} {% /related-content %} --- # Testing Source: https://keystonejs.com/docs/guides/testing When building a web application it's important to be able to test the behaviour of your system to ensure it does what you expect. In this guide we'll show you how to use `@keystone-6/core/testing` and [Vitest](https://vitest.dev/) to write tests that check the behaviour of your GraphQL API. ## What tests might look like Typically you will use tests to verify the behaviour of your code. You might typically test things like your access control, hooks, virtual fields et cetera. The provider-specific `resetDatabase` helpers reset the database and then apply the `migration.sql` files from the migrations directory in order. They create the configured database if it does not exist. They do not read `schema.prisma` or run Prisma `db push`. Before using it, make sure your migrations are up to date with your schema: ```sh keystone build --no-ui prisma migrate dev ``` Running `keystone dev` by itself is not enough because `db push` does not create migration files. See the [database migration guide](./database-migration) for the complete migration workflow. ```typescript import path from 'node:path' import { getContext } from '@keystone-6/core/context' import { resetDatabase } from '@keystone-6/core/testing/sqlite' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { afterAll, beforeEach, expect, test } from 'vitest' import * as PrismaModule from './generated/prisma/client' import baseConfig from './keystone' const dbPath = `./test-${process.env.VITEST_WORKER_ID}.db` const dbUrl = `file:${dbPath}` const config = { ...baseConfig, db: { ...baseConfig.db, prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: dbUrl }) }), }, } beforeEach(async () => { const migrationsDirectory = path.join(__dirname, 'migrations') await resetDatabase({ filename: dbPath }, migrationsDirectory) }) const context = getContext(config, PrismaModule) afterAll(() => context.prisma.$disconnect()) test('Your unit test', async () => { // ... }) ``` {% hint kind="tip" %} We're setting the database URL as `file:./test-${process.env.VITEST_WORKER_ID}.db` so that our tests can use one database per Vitest worker and run each test suite in parallel. {% /hint %} ### Context API The [context API](../context/overview) lets you easily manipulate data in your system. We can use the [Query API](../context/query) to ensure that we can do basic CRUD operations. ```typescript const person = await context.query.Person.createOne({ data: { name: 'Alice', email: 'alice@example.com', password: 'super-secret' }, query: 'id name email password { isSet }', }) expect(person.name).toEqual('Alice') expect(person.email).toEqual('alice@example.com') expect(person.password.isSet).toEqual(true) ``` This API works well when we expect an operation to succeed. If we expect an operation to fail we can use the `context.graphql.raw` operation to check that both the `data` and `errors` returned by a query are what we expect. ```typescript // Create user without the required `name` field const { data, errors } = await context.graphql.raw({ query: `mutation { createPerson(data: { email: "alice@example.com", password: "super-secret" }) { id name email password { isSet } } }`, }) expect(data.createPerson).toBe(null) expect(errors).toHaveLength(1) expect(errors[0].path).toEqual(['createPerson']) expect(errors[0].message).toEqual( 'You provided invalid data for this operation.\n - Person.name: Name must not be empty' ) ``` The `context.withSession()` function can be used to run queries as if you were logged in as a particular user. This can be useful for testing the behaviour of your access control rules. In the example below, the access control only allows users to update their own tasks. ```typescript // Create some users const [alice, bob] = await context.query.Person.createMany({ data: [ { name: 'Alice', email: 'alice@example.com', password: 'super-secret' }, { name: 'Bob', email: 'bob@example.com', password: 'super-secret' }, ], }) // Create a task assigned to Alice const task = await context.query.Task.createOne({ data: { label: 'Experiment with Keystone', priority: 'high', isComplete: false, assignedTo: { connect: { id: alice.id } }, }, }) // Check that we can't update the task when logged in as Bob const { data, errors } = await context.withSession({ itemId: bob.id, data: {} }).graphql.raw({ query: `mutation update($id: ID!) { updateTask(where: { id: $id }, data: { isComplete: true }) { id } }`, variables: { id: task.id }, }) expect(data!.updateTask).toBe(null) expect(errors).toHaveLength(1) expect(errors![0].path).toEqual(['updateTask']) expect(errors![0].message).toEqual( `Access denied: You cannot update the item '{"id":"${task.id}"}' - it may not exist` ) ``` ## Related resources {% related-content %} {% well heading="Example Project: Testing" href="https://github.com/keystonejs/keystone/tree/main/examples/testing" target="_blank" %} Shows you how to write tests against the GraphQL API to your Keystone system. Builds on the Authentication example project. {% /well %} {% well heading="Context API Reference" href="/docs/context/overview" %} The API for run-time functionality in your Keystone system. Use it to write business logic for access control, hooks, testing, GraphQL schema extensions, and more. {% /well %} {% well heading="Query API Reference" href="/docs/context/query" %} A programmatic API for running CRUD operations against your GraphQL API. For each list in your system you get an API at `context.query.`. {% /well %} {% /related-content %} --- # How To Use Document Fields Source: https://keystonejs.com/docs/guides/document-fields The [`document`](../fields/document) field type is a highly customizable rich text editor that lets content creators quickly and easily edit content in your system. It's built with [Slate](https://docs.slatejs.org/), stores your content as JSON-structured data, and lets you do things like: - Configure the types of formatting used in your documents - Easily render the document in your application - Insert relationships to other items in your Keystone database - Define your own custom editor blocks based on React Components To see the document field in action, try out the [demo](./document-field-demo). ## Configuration The document field provides a number of different formatting options, all of which can be configured. To get started with a fully featured editor experience, you can turn on all of the built-in options. ```typescript import { list } from '@keystone-6/core'; import { document } from '@keystone-6/fields-document'; export const lists = { Post: list({ fields: { content: document({ formatting: true, dividers: true, links: true, layouts: [ [1, 1], [1, 1, 1], ], }), }, }), }; ``` This has enabled all of the **formatting** options, enabled inline **links**, section **dividers**, and both 2 and 3 column **layouts**. We can disable any of these features by simply omitting the option from our configuration. ### Formatting Setting `formatting: true` turns on all the formatting options for the document. If you need more fine-grained control over which options are enabled, you can explicitly list the features you want, e.g. ```typescript content: document({ formatting: { inlineMarks: { bold: true, italic: true, underline: true, strikethrough: true, code: true, superscript: true, subscript: true, keyboard: true, }, listTypes: { ordered: true, unordered: true, }, alignment: { center: true, end: true, }, headingLevels: [1, 2, 3, 4, 5, 6], blockTypes: { blockquote: true, code: true }, softBreaks: true, }, }), ``` All the features set to `true` will be enabled in your document field. To disable a specific feature you can simply omit it from the configuration. If you want to enable all the options in a particular sub-group, you can set the group to `true`. For example, to enable all `listType` options you could set `listType: true`. You can experiment with the different configuration settings in the [document field demo](./document-field-demo). ## Querying Each document field will generate a type within your GraphQL schema. The example above of a `content` field in the `Post` list would generate the type: ```graphql type Post_content_DocumentField { document(hydrateRelationships: Boolean! = false): JSON! } ``` To query the content we can run the following GraphQL query, which will return the JSON representation of the content in `posts.content.document`. ```graphql query { posts { content { document } } } ``` We will discuss the `hydrateRelationships` option [below](#inline-relationships). The document data is stored as JSON. You can use the [document field demo](./document-field-demo) to interactively explore how data is stored when you make changes in the document editor. ## Rendering To render the document in a React app, use the `@keystone-6/document-renderer` package. ```tsx import { DocumentRenderer } from '@keystone-6/document-renderer'; ; ``` The `DocumentRenderer` component accepts the `JSON` representation of the document returned by the GraphQL API. ### Overriding the default renderers The `DocumentRenderer` has built in renderers for all the different types of data stored in the JSON formatted data. If you need to override these defaults, you can do so by providing your own renderers to `DocumentRenderer`. ```tsx import { DocumentRenderer, DocumentRendererProps } from '@keystone-6/document-renderer'; const renderers: DocumentRendererProps['renderers'] = { // use your editor's autocomplete to see what other renderers you can override inline: { bold: ({ children }) => { return {children}; }, }, block: { paragraph: ({ children, textAlign }) => { return

{children}

; }, }, }; ; ``` ## Inline Relationships The document field can also have inline relationships which reference other items in your system. For example, you might want to include twitter-style mentions of other users in a blog application. We can achieve this with the `relationships` option to the document field. ```tsx import { config, list } from '@keystone-6/core'; import { document } from '@keystone-6/fields-document'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { Post: list({ fields: { content: document({ relationships: { mention: { listKey: 'Author', label: 'Mention', labelField: 'name', selection: 'id name', }, }, }), }, }), Author: list({ fields: { name: text(), } }), }, }); ``` When you add an inline relationship to your document field, it becomes accessible in the Admin UI behind the `+` icon. This menu uses the `label` specified in the relationship config. ![The Admin UI showing the relationship label behind the plus menu icon](/assets/guides/document-fields/inline-relationship-label.png) You can also access the relationship directly using the `/` command and then starting to type the label. ![The Admin UI showing use of slash (/) to select the inline relationship](/assets/guides/document-fields/inline-relationship-slash.png) You can then select an item from the list specified by `listKey` from the inline select component in the document editor. ![The Admin UI showing the select field used to choose a related item](/assets/guides/document-fields/inline-relationship-select.png) {% hint kind="tip" %} **Tip**: The select component will use the [`ui.labelField`](../config/lists#ui) of the related list in its options list. Make sure you have this value configured to make finding related items easier for your users. {% /hint %} ### Querying inline relationships The document field stores the `id` of the related item in its data structure. If you query for the document, the inline relationship block will include the ID as `data.id`. ```JSON ... { "type": "relationship", "data": { "id": "ckqk4hkcg0030f5mu6le6xydu" }, "relationship": "mention", "children": [{ "text": "" }] }, ... ``` This is generally not very useful if you want to render the item in your document. To obtain more useful data, we can pass the `hydrateRelationships: true` option to our query. ```graphql query { posts { content { document(hydrateRelationships: true) } } } ``` This will add a `data.label` value, based on the related item's label field, and a `data.data` value, which is populated with the data indicated by the `selection` config option. ```JSON ... { "type": "relationship", "data": { "id": "ckqk4hkcg0030f5mu6le6xydu", "label": "Alice", "data": { "id": "ckqk4hkcg0030f5mu6le6xydu", "name": "Alice" } }, "relationship": "mention", "children": [{ "text": "" } }, ... ``` {% hint kind="warn" %} **Null data:** It is possible to add an inline relationship in the document editor without actually selecting a related item. In these cases, the value of `data` will be `null`. {% /hint %} {% hint kind="warn" %} **Dangling references:** The data for relationships are stored as IDs within the JSON data structure of the document. If an item in your database is deleted, the document field will not have any knowledge of this, and you will be left with a dangling reference in your document data. In other instances the person querying for the document may not have read access to the related item. In both these cases the `data.label` and `data.data` values will be `undefined`. {% /hint %} ### Rendering inline relationships The `DocumentRenderer` has a rudimentary renderer built in for inline relationships which simply returns the `data.label` (or `data.id` if `hydrateRelationships` is `false`) inside a `` tag. This is unlikely to be what you want, so you will need to define a custom renderer for your relationship. A custom renderer for our `mention` relationship might look like: ```typescript import { DocumentRenderer, DocumentRendererProps } from '@keystone-6/document-renderer'; const renderers: DocumentRendererProps['renderers'] = { inline: { relationship({ relationship, data }) { if (relationship === 'mention') { if (data === null || data.data === undefined) { return [unknown author] } else { return {data.data.name}; } } return null; }, }, }; ; ``` The `relationship` argument lets you write renderers for each of the different relationships defined in your document. The `data` argument is provided directly from the query, and we can use the properies of `data.data` to render our mentions as links to the author's page. {% hint kind="warn" %} **Missing data:** Make sure your renderer checks for `data === null` (no item selected) and `data.data === undefined` (selected item not found) and handles these cases appropriately. {% /hint %} ## Component Blocks Component blocks let you add custom blocks to the editor that can accept unstructured content and render a form that renders arbitrary React components for input. To add component blocks, you need to create a file somewhere and export component blocks from there `component-blocks.tsx` ```tsx import React from 'react'; import { NotEditable, component, fields } from '@keystone-6/fields-document/component-blocks'; // naming the export componentBlocks is important because the Admin UI // expects to find the components like on the componentBlocks export export const componentBlocks = { quote: component({ preview: (props) => { return (
{props.fields.content.element}
— {props.fields.attribution.element}
); }, label: 'Quote', schema: { content: fields.child({ kind: 'block', placeholder: 'Quote...', formatting: { inlineMarks: 'inherit', softBreaks: 'inherit' }, links: 'inherit', }), attribution: fields.child({ kind: 'inline', placeholder: 'Attribution...' }), }, chromeless: true, }), }; ``` You need to import the `componentBlocks` and pass it to the document field along with the path to the file with the component blocks to `ui.views`. `keystone.ts` ```ts import { config, list } from '@keystone-6/core'; import { document } from '@keystone-6/fields-document'; import { componentBlocks } from './component-blocks'; export default config({ lists: { ListName: list({ fields: { fieldName: document({ ui: { views: './component-blocks' }, componentBlocks, }), }, }), }, }); ``` {% summary %} In the `examples/document-field-customisation` example, the Quote, Notice, Hero and Checkbox List items are written as component blocks. {% /summary %} ### Fields There are a variety of fields you can configure. #### Child Fields Child fields allow you to embed an editable region inside of a component block preview. They can either have `kind: 'inline'`, or `kind: 'block'`. This refers to **what elements can be inside of them**. `kind: 'inline'` child fields can only contain text with marks/links/inline relationships. `kind: 'block'` child fields contain block-level elements such as paragraph, lists, etc. They require a placeholder which is shown when there is no text inside the child field. The placeholder is required though it can be an empty string if it'll be clear to editors that the location of the child field is editable. By default, child fields can only contain plain text, if you'd like to enable other features of the document editor inside a child field, you can enable the features similarly to enabling them in the document field config. Unlike the document field config though, these options accept `'inherit'` instead of `true`, this is because if `'inherit'` is set then that feature will be enabled **if it's also enabled at the document field config level** so you can't enable features in a child field but not in the rest of the document field. Block child fields (`kind: 'block'`) also support `componentBlocks: 'inherit'` to allow component blocks to be nested inside a child field. In the preview, child fields appear as React nodes that should be rendered. Note that you **must** render child fields in the preview. If they are not rendered, you will receive errors. ```tsx import { NotEditable, component, fields } from '@keystone-6/fields-document/component-blocks'; component({ preview: (props) => { return (
{props.fields.content.element}
— {props.fields.attribution.element}
); }, label: 'Quote', schema: { content: fields.child({ kind: 'block', placeholder: 'Quote...', formatting: { inlineMarks: 'inherit', softBreaks: 'inherit' }, links: 'inherit', }), attribution: fields.child({ kind: 'inline', placeholder: 'Attribution...' }), }, chromeless: true, }) ``` > Note: You have to be careful to wrap `NotEditable` around other elements in the preview but you cannot wrap it around a child field. #### Form Fields `@keystone-6/core/component-blocks` ships with a set of form fields for common purposes: - `fields.text({ label: '...', defaultValue: '...', displayMode: 'input' | 'textarea' })` - `fields.integer({ label: '...', defaultValue: '...' })` - `fields.url({ label: '...', defaultValue: '...' })` - `fields.select({ label: '...', options: [{ label:'A', value:'a' }, { label:'B', value:'b' }] defaultValue: 'a' })` - `fields.checkbox({ label: '...', defaultValue: false })` You can write your own form fields that conform to this API. ```ts type FormField = { kind: 'form'; Input(props: { value: Value; onChange(value: Value): void; autoFocus: boolean; /** * This will be true when validate has returned false and the user has attempted to close the form * or when the form is open and they attempt to save the item */ forceValidation: boolean; }): ReactElement | null; /** * The options are config about the field that are available on the * preview props when rendering the toolbar and preview component */ options: Options; defaultValue: Value; /** * validate will be called in two cases: * - on the client in the editor when a user is changing the value. * Returning `false` will block closing the form * and saving the item. * - on the server when a change is received before allowing it to be saved * if `true` is returned * @param value The value of the form field. You should NOT trust * this value to be of the correct type because it could come from * a potentially malicious client */ validate(value: unknown): boolean; }; ``` #### Object Fields To nest a group of component block fields, you can use `fields.object` ```tsx import { fields } from '@keystone-6/fields-document/component-blocks'; fields.object({ a: fields.text({ label: 'A' }), b: fields.text({ label: 'B' }), }); ``` #### Relationship Fields To use relationship fields on component blocks, you need to add a relationship field and provide a list key, label and options selection. In the form, it will render a relationship select like the relationship field on lists. Similarly to inline relationships, they will be hydrated with this selection if `hydrateRelationships: true` is provided when fetching. {% hint kind="warn" %} We recently updated the config API for inline relationships in component blocks. If you’re using a version prior to 2022-03-25 please [read this](https://github.com/keystonejs/keystone/releases/tag/2022-03-25) upgrade guidance. {% /hint %} ```tsx import { fields } from '@keystone-6/fields-document/component-blocks'; ... someField: fields.relationship({ label: 'Authors', description: 'The authors of this post', listKey: 'Author', labelField: 'name', selection: 'id name posts { title }', many: true, }); ... ``` > Note: Like inline relationships, relationship fields on component blocks are not stored like relationship fields on lists, they are stored as ids in the document structure. ### Objects ```tsx import { fields } from '@keystone-6/fields-document/component-blocks'; fields.object({ text: fields.text({ label: 'Text' }), child: fields.placeholder({ placeholder: 'Content...' }), }); ``` ### Conditional Fields You can conditionally show different fields with `fields.conditional`, they require a form field with a value that is either a string or a boolean as the discriminant and an object of fields for the values. ```tsx import { fields } from '@keystone-6/fields-document/component-blocks'; fields.conditional(fields.checkbox({ label: 'Show Call to action' }), { true: fields.object({ url: fields.url({ label: 'URL' }), content: fields.child({ kind: 'inline', placeholder: 'Call to Action' }), }), false: fields.empty(), }); ``` > You might find `fields.empty()` useful which stores and renders nothing if you want to have a field in one case and nothing anything in another ### Array Fields Array fields allow you to store an array of another component block field type. ```tsx import { fields } from '@keystone-6/fields-document/component-blocks'; fields.array(fields.object({ isComplete: fields.checkbox({ label: 'Is Complete' }), content: fields.child({ kind: 'inline', placeholder: '' }), })) ``` ### Chromeless If you want to hide the default UI around your component block, you can set `chromeless: true`. This removes the border, toolbar, and generated form. ```jsx quote: component({ preview: ({ attribution, content }) => { ... }, label: 'Quote', schema: { ... }, chromeless: true, }), ``` You can see the differences between each below: ![Quote component blocks demonstrating chrome, and chromeless styling](/assets/guides/document-fields/chomeless-example-docs-demo.png) In the [document editor demo](/docs/guides/document-field-demo), the Notice and Quote blocks are chromeless, but the Hero block has the standard chrome styling. ### Rendering Component blocks #### Typing props for rendering component blocks If you're using TypeScript, you can infer the props types for component with `InferRenderersForComponentBlocks` from `@keystone-6/fields-document/component-blocks`. ```tsx import { DocumentRenderer } from '@keystone-6/document-renderer'; import { InferRenderersForComponentBlocks } from '@keystone-6/fields-document/component-blocks'; import { componentBlocks } from '../path/to/your/custom/views'; const componentBlockRenderers: InferRenderersForComponentBlocks = { someComponentBlock: props => { // props will be inferred from your component blocks }, }; ; ``` ## Related resources {% related-content %} {% well heading="Example: Document Field" href="https://github.com/keystonejs/keystone/tree/main/examples/document-field" target="_blank" %} Illustrates how to configure `document` fields in your Keystone system and render their data in a frontend application. Builds on the Blog starter project. {% /well %} {% well heading="Example: Document Field Customisation" href="https://github.com/keystonejs/keystone/tree/main/examples/document-field-customisation" %} Example to demonstrate customisation of Keystone's document field and document renderer. {% /well %} {% well heading="Document Field Demo" href="/docs/guides/document-field-demo" %} Test drive the many features of Keystone’s Document field on this website. {% /well %} {% /related-content %} --- # Virtual Fields Source: https://keystonejs.com/docs/guides/virtual-fields Keystone lets you define your data model in terms of `lists`, which have `fields`. Most lists will have some [scalar fields](../fields/overview#scalar-types), such as `text` and `integer` fields, which are stored in your database. It can also be helpful to have read-only fields which are computed on the fly when you query them. Keystone lets you do this with the [`virtual`](../fields/virtual) field type. Virtual fields provide a powerful way to extend your GraphQL API. In this guide we'll introduce the syntax for adding virtual fields, and show how to build up from a simple to a complex example. ## Hello world We'll start with a list called `Example` and create a virtual field called `hello`. ```typescript import { config, createSchema, g, list } from '@keystone-6/core'; import { virtual } from '@keystone-6/core/fields'; export default config({ lists: { Example: list({ fields: { hello: virtual({ field: g.field({ type: g.String, resolve() { return "Hello, world!"; }, }), }), }, }), }, }); ``` We can now run a GraphQL query and request the `hello` field on one of our `Example` items, ```graphql { example(where: { id: "1" }) { id hello } } ``` which gives the response: ```javascript { example: { id: "1", hello: "Hello, world! } } ``` The value of `hello` is generated from the `resolve` function, which returns the string `"Hello, world!"`. ## The `g` API The `virtual` field is configured using functions from the `g` export from `@keystone-6/core`. This API provides the interface required to create type-safe extensions to the Keystone GraphQL schema. The `g` API is based on the [`@graphql-ts/schema`](https://github.com/Thinkmill/graphql-ts) package. The `virtual` field accepts a configuration option called `field`, which is a `g.field()` object. In our example we passed in two required options to `g.field()`. The option `type: g.String` specifies the GraphQL type of our virtual field, and `resolve() { ... }` defines the [GraphQL resolver](https://graphql.org/learn/execution/#root-fields-resolvers) to be executed when this field is queried. The `g` API provides support for the built in GraphQL scalar types `Int`, `Float`, `String`, `Boolean`, and `ID`, as well as the Keystone custom scalars `Upload` and `JSON`. ## Resolver arguments The `resolve` function accepts arguments which let you write more sophisticated virtual fields. The arguments are `(item, args, context, info)`. The `item` argument is the **internal item** representing the list item being queried. The `args` argument represents the arguments passed to the field itself in the query. The `context` argument is a [`KeystoneContext`](../context/overview) object. The `info` argument holds field-specific information relevant to the current query as well as the schema details. We can use the `item` and `context` arguments to query data in our Keystone system. For example, if we have a blog with `Author` and `Post` lists, it might be convenient to have an `authorName` field on the `Post` list. We can do this with a `virtual` field which queries for the related `author` and returns their name. ```typescript export default config({ lists: { Post: list({ fields: { content: text(), author: relationship({ ref: 'Author', many: false }), authorName: virtual({ field: g.field({ type: g.String, async resolve(item, args, context) { const { author } = await context.query.Post.findOne({ where: { id: item.id.toString() }, query: 'author { name }', }); return author && author.name; }, }), }), }, }), Author: list({ fields: { name: text({ validation: { isRequired: true } }), }, }), }, }); ``` ## GraphQL arguments Continuing with our blog example, we may want to extract an excerpt of each blog post for display on the home page. We could query the full `Post.content` field for each post and then slice it in the client, but it would be nicer if we could ask for just the slice that we want from the GraphQL API. To do this we can use a `virtual` field which takes a `length` argument and then performs the `.slice()` operation as part of resolver function. This gives control of the size of the excerpt to the frontend while getting the backend to do the actual work. We use the `args` option to define the GraphQL field arguments we want to support. ```typescript export default config({ lists: { Post: list({ fields: { content: text(), excerpt: virtual({ field: g.field({ type: g.String, args: { length: g.arg({ type: g.nonNull(g.Int), defaultValue: 200 }), }, resolve(item, { length }) { if (!item.content) { return null; } const content = item.content as string; if (content.length <= length) { return content; } else { return content.slice(0, length - 3) + '...'; } }, }), ui: { query: '(length: 500)' }, }), }, }), }, }); ``` This will generate the following GraphQL type: ```graphql type Post { id: ID! content: String excerpt(length: Int! = 200): String } ``` We can now perform the following query to get all the excerpts without over-fetching on the client. ```graphql { posts { id excerpt(length: 100) } } ``` As well as passing in the `field` definition, we have also passed in `ui: { query: '(length: 500)' }`. This is the value used when displaying the field in the Admin UI, where we want to have a different length the default of `200`. Had we not specified `defaultValue` in our field, the `ui.query` argument would be **required**, as the Admin UI would not be able to query this field without it. ## GraphQL objects The examples above returned a scalar `String` value. Virtual fields can also be configured to return a GraphQL object. In our blog example we might want to provide some statistics on each blog post, such as the number of words, sentences, and paragraphs in the post. We can set up a GraphQL type called `PostCounts` to represent this data using the `g.object()` function. ```typescript export default config({ lists: { Post: list({ fields: { content: text(), counts: virtual({ field: g.field({ type: g.object<{ words: number; sentences: number; paragraphs: number; }>()({ name: 'PostCounts', fields: { words: g.field({ type: g.Int }), sentences: g.field({ type: g.Int }), paragraphs: g.field({ type: g.Int }), }, }), resolve(item: any) { const content = item.content || ''; return { words: content.split(' ').length, sentences: content.split('.').length, paragraphs: content.split('\n\n').length, }; }, }), ui: { query: '{ words sentences paragraphs }' }, }), }, }), }, }); ``` This example is written in TypeScript, so we need to specify the type expected by the `PostCounts` type. This type must correspond to the return type of the `resolve` function. Because our `virtual` field has an object type, we also need to provide a value for the option `ui.query`. This fragment tells the Keystone Admin UI which values to show in the item page for this field. #### Self-referencing objects {% hint kind="tip" %} This information is specifically for TypeScript users of the `g.object()` function with a self-referential GraphQL type. {% /hint %} GraphQL types will often contain references to themselves and to make TypeScript allow that, you need have an explicit type annotation of `g.ObjectType` along with making `fields` a function that returns the object. ```ts type PersonSource = { name: string; friends: PersonSource[] }; const Person: g> = g.object()({ name: "Person", fields: () => ({ name: g.field({ type: g.String }), friends: g.field({ type: g.list(Person) }), }), }); ``` ## Keystone types Rather than returning a custom GraphQL object, we might want to have a virtual field which returns one of the GraphQL types generated by Keystone itself. For example, for each `Author` we might want to return their `latestPost` as a `Post` object. To achieve this, rather than passing in `g.field({ ... })` as the `field` option, we pass in a function `lists => g.field({ ... })`. The argument `lists` contains the type information for all of the Keystone lists. In our case, we want the output type of the `Post` list, so we specify `type: lists.Post.types.output`. ```ts export const lists = { Post: list({ fields: { title: text(), content: text(), publishDate: timestamp(), author: relationship({ ref: 'Author.posts', many: false }), }, }), Author: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ isIndexed: 'unique', validation: { isRequired: true } }), posts: relationship({ ref: 'Post.author', many: true }), latestPost: virtual({ field: lists => g.field({ type: lists.Post.types.output, async resolve(item, args, context) { const { posts } = await context.query.Author.findOne({ where: { id: item.id.toString() }, query: `posts( orderBy: { publishDate: desc } take: 1 ) { id }`, }); if (posts.length > 0) { return context.db.Post.findOne({ where: { id: posts[0].id } }); } }, }), ui: { query: '{ title publishDate }' }, }), }, }), }; ``` Once again we need to specify `ui.query` on this virtual field to specify which fields of the `Post` to display in the Admin UI. ## Working with virtual fields Virtual fields provide a powerful way to extend your GraphQL API, however there are some considerations to keep in mind when using them. The virtual field executes its resolver every time the field is requested. For trivial calculations this isn't a problem, but for more complex calculations this can lead to performance issues. In this case you can consider memoising the value to avoid recalculating it for each query. Another way to address this is to use a [scalar field](../fields/overview#scalar-types) and to populate its value each time the item is updated using a [hook](./hooks). The other main consideration is that it is not possible to filter on a virtual field, as each item calcutes its value dynamically, rather than having it stored in the database. Using a pre-calculated scalar field is the best solution to use if you need filtering for your field. ## Related resources {% related-content %} {% well heading="Virtual fields example" href="https://github.com/keystonejs/keystone/tree/main/examples/virtual-field" target="_blank" %} A demo project that shows you how to add virtual fields to a Keystone list. {% /well %} {% well heading="Virtual Fields: API Reference" href="/docs/fields/virtual" %} A virtual field represents a value which is computed a read time, rather than stored in the database. {% /well %} {% /related-content %} --- # Custom Fields Source: https://keystonejs.com/docs/guides/custom-fields Keystone provides a collection of [field types](../fields/overview) which you can use to build your system. If you need a field type which isn't provided, or you need a specialised version of an existing field type, you can define your own custom field type. There are two parts to a field type: - The **backend** portion which defines what data is stored in the database and how it appears in the GraphQL API. - The **frontend** portion which defines how the field looks and behaves in the Admin UI. The general approach to creating a custom field type is to take an existing field type and make the appropriate changes for your use case. In this guide we're going to create a field type `myInt` which recreates the [`integer`](https://github.com/keystonejs/keystone/tree/main/packages/core/src/fields/types/integer) field type. {% hint kind="tip" %} For inspiration, see [the source for the fields that Keystone provides](https://github.com/keystonejs/keystone/tree/main/packages/core/src/fields/types) and the [Custom Fields](https://github.com/keystonejs/keystone/tree/main/examples/custom-field) example project. {% /hint %} ## Backend The backend portion is the entry point to the field type. We define our field type `myInt` and the corresponding type `MyIntFieldConfig` which defines the accepted configuration options. ```ts import { BaseListTypeInfo, FieldTypeFunc, CommonFieldConfig, fieldType, orderDirectionEnum, } from '@keystone-6/core/types' import { g } from '@keystone-6/core' export type MyIntFieldConfig = CommonFieldConfig & { isIndexed?: boolean | 'unique' } export function myInt ({ isIndexed, ...config }: MyIntFieldConfig = {}): FieldTypeFunc { return meta => fieldType({ kind: 'scalar', mode: 'optional', scalar: 'Int', index: isIndexed === true ? 'index' : isIndexed || undefined, })({ ...config, input: { create: { arg: g.arg({ type: g.Int }) }, update: { arg: g.arg({ type: g.Int }) }, orderBy: { arg: g.arg({ type: orderDirectionEnum }) }, }, output: g.field({ type: g.Int }), views: './view', }) } ``` ### DB Field `fieldType` is called with the _db field_ which defines what the field should store in the database. Here it's an integer (`scalar: 'Int'`) but there are other kinds which you can find in [the type definitions for `DBField`](https://github.com/keystonejs/keystone/blob/main/packages/core/src/types/next-fields.ts). ### Inputs The `input` object defines the GraphQL inputs for the field type. ```ts input: { create: { arg: g.arg({ type: g.Int }) }, update: { arg: g.arg({ type: g.Int }) }, orderBy: { arg: g.arg({ type: orderDirectionEnum }) }, }, ``` You can also provide resolvers to transform the value coming from GraphQL into the value that is passed to Prisma. ```ts input: { create: { arg: g.arg({ type: g.Int }), resolve: (val, context) => val }, update: { arg: g.arg({ type: g.Int }), resolve: (val, context) => val }, orderBy: { arg: g.arg({ type: orderDirectionEnum }), resolve: (val, context) => val }, }, ``` ### Output The output field defines what can be fetched from the field: ```ts output: g.field({ type: g.Int }) ``` A resolver can also be provided: ```ts output: g.field({ type: g.Int, resolve({ value, item }, args, context, info) { return value } }) ``` ## Frontend The frontend portion of a field must be in a seperate file that the backend implementation points to with the `views` option. The `views` option is resolved as though it is an import from some file in the project directory. ``` views: './view', ``` #### Controller The `controller` export defines the functional parts of the frontend of a field. ```tsx // view.tsx export const controller = (config: FieldControllerConfig): FieldController => { return { path: config.path, label: config.label, graphqlSelection: config.path, defaultValue: '', deserialize: data => { const value = data[config.path] return typeof value === 'number' ? value + '' : '' }, serialize: value => ({ [config.path]: value === '' ? null : parseInt(value, 10) }), } } ``` ### Field The `Field` export is a React component which is used in the **item view** and the **create modal** that allows users to view and edit the value of the field. ```tsx // view.tsx import { TextField } from '@keystar/ui/text-field' export function Field ({ field, value, onChange, autoFocus }: FieldProps) { const disabled = onChange === undefined return ( onChange?.(x === '' ? null : x)} value={value ?? ''} /> ) } ``` ### Cell The `Cell` export is an optional custom React component for formatting the field value as shown in the table on the **list view**. This component does not allow modifying the value. ```tsx // view.tsx import { CellLink, CellContainer } from '@keystone-6/core/admin-ui/components' import { CellComponent } from '@keystone-6/core/types' export const Cell: CellComponent = ({ value }) => { return {value} } ``` ## Related resources {% related-content %} {% well heading="Example Project: Custom Fields" href="https://github.com/keystonejs/keystone/tree/main/examples/custom-field" target="_blank" %} Adds a custom field type based on the `integer` field type which lets users rate items on a 5-star scale. Builds on the Blog starter project. {% /well %} {% /related-content %} --- # Custom Admin UI Logo Source: https://keystonejs.com/docs/guides/custom-admin-ui-logo In this guide we'll show you how to add a custom logo to the Keystone Admin UI. {% hint kind="tip" %} Presently the only replaceable component in the Admin UI is the logo. If you have suggestions as to other components you would like to be able to customise, let us know on [Slack](https://community.keystonejs.com) {% /hint %} To import a custom component for rendering within the Admin UI, ensure that the `admin/config.tsx` file exists in the root of your Keystone project. This file expects a named export `components`. ```tsx // admin/config.tsx function CustomLogo () { return

Custom Logo here

} export const components = { Logo: CustomLogo } ``` If you have styling constraints, we recommend using `@keystar/ui` components, which is the design system used internally by Keystone's Admin UI. ```tsx // admin/config.tsx import { Heading } from '@keystar/ui/typography'; function CustomLogo () { return

Custom Logo here

} export const components = { Logo: CustomLogo } ``` Of course this is purely a recommendation, if you would prefer to roll your own css-in-js solution in with your custom component please feel free to! Although this may require additional configuration outside of the scope of this guide. Once you've added your custom logo component you should see it rendered out in the Admin UI. ![image of Admin UI with custom logo](/assets/guides/custom-admin-ui-logo/custom-logo-result.png) ## Related resources {% related-content %} {% well heading="Example: Custom Admin UI Logo" href="https://github.com/keystonejs/keystone/tree/main/examples/custom-admin-ui-logo" target="_blank" %} Adds a custom logo component in the Admin UI. Builds on the Task Manager starter project. {% /well %} {% well heading="Custom Admin UI Navigation Guide" href="/docs/guides/custom-admin-ui-navigation" %} Learn how to create your own custom Navigation components in Keytone’s Admin UI. {% /well %} {% well heading="Custom Admin UI Pages Guide" href="/docs/guides/custom-admin-ui-pages" %} Learn how to add your own custom pages to Keystone’s Admin UI. {% /well %} {% /related-content %} --- # Custom Admin UI Pages Source: https://keystonejs.com/docs/guides/custom-admin-ui-pages ## Getting Started In this guide we'll show you how to add custom pages to the Keystone Admin UI. As the Admin UI is built on top of [Next.js](https://nextjs.org/docs/basic-features/pages), it exposes the same pages directory for adding custom pages. To create a custom page, ensure that the `admin/pages` directory exists in the root of your Keystone Project. Much like with Next.js, all files in this directory will be added as routes to the Admin UI. The default export of every file in this directory is expected to be a valid React Component rendered out as the contents of the route. ```tsx // admin/pages/custom-page.tsx import Link from 'next/link' export default function CustomPage () { return ( <>

This is a custom Admin UI Page

It can be accessed via the route /custom-page

) } ``` {% hint kind="error" %} **Not all Next.js exports are available:** Keystone **only** supports the page component as a default export in the pages directory. This means that unlike with Next, auxillary exports such as `getStaticProps` and `getServerProps` are not supported. {% /hint %} With this in place, we now have a nice simple custom Admin UI page at `http://localhost:3000/custom-page`. ![example of a simple custom-page in the Admin UI](/assets/guides/custom-admin-ui-pages/simple-custom-page.png) ## Adding Admin UI layout At the moment this page is pretty bare bones. We want our page to look more like an Admin UI page. Keystone helps us do this via the `PageContainer` component exported from `@keystone-6/core/admin-ui/components`. The `PageContainer` component takes a `header` prop, which is expected to be a `ReactElement`. This `header` prop is rendered out as the page title at the top of the page. ```tsx // admin/pages/custom-page.tsx import Link from 'next/link' import { PageContainer } from '@keystone-6/core/admin-ui/components' export default function CustomPage () { return (

This is a custom Admin UI Page

It can be accessed via the route /custom-page

) } ``` With the above snippet, our custom page looks a lot more like the other pages in the Admin UI. ![example of the custom Admin UI page with the PageContainer component](/assets/guides/custom-admin-ui-pages/custom-page-w-page-container.png) There's still a problem though, the header doesn't look right. If we compare the header of our custom page with the header for the Dashboard, there's quite a bit of difference in the styling and font-weight. ![example of the Dashboard header element](/assets/guides/custom-admin-ui-pages/header-prop.png) Keystone pages leverage components from `@keystar/ui` to style the header, so let's use this to give our header the same styling. ```tsx // admin/pages/custom-page.tsx. import { PageContainer } from '@keystone-6/core/admin-ui/components' import { Heading } from '@keystar/ui/typography' export default function CustomPage () { return ( Custom Page}>

This is a custom Admin UI Page

It can be accessed via the route `/custom-page`

) } ``` Much better, our custom page looks and feels like an Admin UI page now. ![custom page with correctly styled header](/assets/guides/custom-admin-ui-pages/custom-page-with-styled-header.png) ## Custom route in Admin UI Navigation Yes, our custom page is looking pretty great, and much more like an Admin UI page, but it's not visible as a navigation item. We can fix this by adding a custom Navigation component with a route pointing to our custom page. First add the following files to the `/admin` directory in the root of your Keystone project. ```tsx // admin/config.ts import type { AdminConfig } from '@keystone-6/core/types' import { CustomNavigation } from './components/CustomNavigation' export const components: AdminConfig['components']= { Navigation: CustomNavigation } ``` ```tsx // admin/components/CustomNavigation.tsx import { NavigationContainer, ListNavItems, NavItem } from '@keystone-6/core/admin-ui/components' import type { NavigationProps } from '@keystone-6/core/admin-ui/components' export function CustomNavigation({ lists }: NavigationProps) { return ( Dashboard ) } ``` {% hint kind="tip" %} You will need to restart your Keystone system after adding `admin/config.ts` for the custom Navigation component to be loaded. {% /hint %} {% hint kind="tip" %} If you're interested in more details on creating a custom Navigation component check out the [Custom Admin UI Navigation](/docs/guides/custom-admin-ui-navigation) guide. {% /hint %} Lastly we'll add our new route to the newly created `CustomNavigation` component. ```tsx // admin/components/CustomNavigation.tsx import { NavigationContainer, ListNavItems, NavItem } from '@keystone-6/core/admin-ui/components' import type { NavigationProps } from '@keystone-6/core/admin-ui/components' export function CustomNavigation({ lists }: NavigationProps) { return ( Dashboard Custom Page ) } ``` {% hint kind="tip" %} Under the hood Keystone's Admin UI is powered by Next.js, so the route to our custom page is the filename of our custom page component. In this case it's `/custom-page`. {% /hint %} With all that in place, our custom Admin UI page is now navigable from the Admin UI Navigation component, and we can access it from other pages in the Admin UI. ![completed custom Admin UI page](/assets/guides/custom-admin-ui-pages/custom-page-completed.png) ## Styling There are other styling considerations when adding a custom page to the Admin UI that go beyond making it _look_ and _feel_ like an Admin UI page. For this, we recommend using `@keystar/ui`, which is the design system used internally by Keystone's Admin UI. The snippet below uses `@keystar/ui` components to help add some basic alignment and layout styling to the contents of our Admin UI custom page. ```tsx // admin/pages/custom-page.tsx import Link from 'next/link' import { PageContainer } from '@keystone-6/core/admin-ui/components' import { Heading } from '@keystar/ui/typography' export default function CustomPage () { return ( Custom Page )}>

This is a custom Admin UI Page

It can be accessed via the route /custom-page

) } ``` Using `emotion` for styling is purely a recommendation, if you would prefer to use another css-in-js or css solution for your custom component please feel free to. This may require additional configuration currently outside of the scope of this guide. ## Related resources {% related-content %} {% well heading="Example: Custom Admin UI Pages" href="https://github.com/keystonejs/keystone/tree/main/examples/custom-admin-ui-pages" target="_blank" %} Adds a custom page in the Admin UI. Builds on the Task Manager starter project. {% /well %} {% well heading="Custom Admin UI Logo Guide" href="/docs/guides/custom-admin-ui-logo" %} Learn how to add your own custom logo to Keystone’s Admin UI. {% /well %} {% well heading="Custom Admin UI Navigation Guide" href="/docs/guides/custom-admin-ui-navigation" %} Learn how to create your own custom Navigation components in Keytone’s Admin UI. {% /well %} {% /related-content %} --- # Custom Admin UI Navigation Source: https://keystonejs.com/docs/guides/custom-admin-ui-navigation In this guide, we'll show you how to create a custom Navigation component to be rendered in the Admin UI. {% hint kind="tip" %} By the end of this guide you should have a custom Navigation component rendering in the Admin UI with a custom route pointing to the KeystoneJS docs. ![finished custom navigation example](/assets/guides/custom-admin-ui-navigation/final-result.png) {% /hint %} ## Adding the custom navigation component to the Admin UI The first thing you'll need to do is to specify an `/admin/config.ts` file in the root of your Keystone project. Reference your custom Navigation component in the export as so. ```ts // /admin/config.ts import type { AdminConfig } from '@keystone-6/core/types' import { CustomNavigation } from './components/CustomNavigation' export const components: AdminConfig['components'] = { Navigation: CustomNavigation } ``` ## Creating your custom Navigation component The next step is to create our `CustomNavigation` React component. In the root of your project create a new tsx file at `/admin/components/CustomNavigation.tsx`. Here we'll export our `CustomNavigation` component as a named export. ```tsx // admin/components/CustomNavigation.tsx import type { NavigationProps } from '@keystone-6/core/admin-ui/components' export function CustomNavigation({ lists }: NavigationProps) { return ( {/* ... */} ) } ``` Keystone will pass the following props to this component. ```tsx type NavigationProps = { lists: ListMeta[] } ``` {% hint kind="tip" %} For more information on the props, please see the [Navigation Props](#navigation-props) section of this guide. {% /hint %} ### Setting up the layout Next we'll want to import some components that Keystone provides to help us build our custom Navigation. ```tsx import { NavigationContainer, NavItem, ListNavItems } from '@keystone-6/core/admin-ui/components' ``` The `NavigationContainer` component provides a container around your navigation links. - Make our `CustomNavigation` component look and feel like the default Admin UI Navigation component. ```tsx import { NavigationContainer, NavItem, ListNavItems } from '@keystone-6/core/admin-ui/components' import type { NavigationProps } from '@keystone-6/core/admin-ui/components' export function CustomNavigation({ lists }: NavigationProps) { return ( {/* ... */} ) } ``` {% hint kind="tip" %} For more information on the `NavigationContainer` see the [NavigationContainer](#navigation-container) section of the [component docs](#components) below. {% /hint %} ### Rendering NavItems for Keystone lists The `ListNavItems` component takes the provided Array of `lists` and renders a list of NavItems. We'll use this component to help us easily create NavItems from Keystone lists. ```tsx import { NavigationContainer, NavItem, ListNavItems } from '@keystone-6/core/admin-ui/components' import type { NavigationProps } from '@keystone-6/core/admin-ui/components' export function CustomNavigation({ lists }: NavigationProps) { return ( {/* ... */} ) } ``` {% hint kind="tip" %} For more information on the `ListNavItems` component, see the [ListNavItems](#list-nav-items) section of the [component docs](#components) below. {% /hint %} ### Adding additional routes The `NavItem` component is a thin styling and accessibility wrapper around the `Link` component from Next.js. We'll use this component to render our custom route as well as the `Dashboard` route. ```tsx import { NavigationContainer, NavItem, ListNavItems } from '@keystone-6/core/admin-ui/components' import type { NavigationProps } from '@keystone-6/core/admin-ui/components' export function CustomNavigation({ lists }: NavigationProps) { return ( Dashboard Keystone Docs ) } ``` {% hint kind="tip" %} For more information on the `NavItem` component, see the [NavItem](#nav-item) section of the [component docs](#components) below. {% /hint %} {% hint kind="warn" %} **Note** When opting into a custom Navigation component you will need to specify a NavItem for the `Dashboard` page (the `/` route). {% /hint %} ### Putting it all together With all that done, your Custom Navigation component should be good to go, and your `/admin` folder should look like this. ```tsx // admin/components/CustomNavigation.tsx import { NavigationContainer, NavItem, ListNavItems } from '@keystone-6/core/admin-ui/components' import type { NavigationProps } from '@keystone-6/core/admin-ui/components' export function CustomNavigation({ lists }: NavigationProps) { return ( Dashboard Keystone Docs ) } // admin/config.ts import { AdminConfig } from '@keystone-6/core/types' import { CustomNavigation } from './components/CustomNavigation' export const components: AdminConfig['components'] = { Navigation: CustomNavigation } ``` Start up your Keystone project, and you should see Custom Navigation with a route to the KeystoneJS docs in the Admin UI. ![finished custom navigation example](/assets/guides/custom-admin-ui-navigation/final-result.png) {% hint kind="tip" %} The rest of this guide will elaborate on some of the details around the helper components Keystone provides and the Navigation props that we glossed over in the main guide. {% /hint %} ## Navigation props This section is to provide more granular information around the props that Keystone passes to your Custom Navigation component. ```tsx export const CustomNavigation = ({ lists }) => {} ``` Keystone passes the following props to your custom Navigation component: ```ts type NavigationProps { lists: ListMeta[] } ``` `lists` is an array of Keystone list objects. Internally Keystone filters through your lists and ensures that only visible lists are passed through to you in the `lists` array prop. ```typescript type ListMeta = { /** Used for optimising the generated list of NavItems in React */ key: string /** href to the list route in the Admin UI. */ path: string /** Used as the label for each list generated NavItem */ label: string /** Other properties exists, but these are the ones that are relevant to the Navigation implementation */ } type Lists = ListMeta[] ``` ## Components Keystone exposes a variety of helper components to make building out your custom Admin UI Navigation component easier. These are: - [NavigationContainer](#navigation-container) - [ListNavItems](#list-nav-items) - [NavItem](#nav-item) ### NavigationContainer This component renders containing markup around your navigation links. ```tsx import { NavigationContainer} from '@keystone-6/core/admin-ui/components' export function CustomNavigation ({ lists }) { return ( {/* ... */} ) } ``` ### ListNavItems This component takes the provided array of `lists` and renders a list of `NavItems`. ```tsx import { ListNavItems } from '@keystone-6/core/admin-ui/components' ``` The `lists` object has the following type: ```typescript type ListNavItemProps = { lists: ListMeta[] include: string[] } ``` If an `include` prop is supplied, the component will only render out lists that match the list `keys` specified in the `include` array. ```tsx const CustomNavigation = ({ lists }) => ( ) ``` ![example of navigation with include prop values](/assets/guides/custom-admin-ui-navigation/listNavItems-with-include.png) Otherwise, all lists will be added. ```tsx const CustomNavigation = ({ lists }) => ( ) ``` ![example of navigation without include prop values](/assets/guides/custom-admin-ui-navigation/listNavItems-without-include.png) ### NavItem This component is a thin styling and accessibility wrapper around the `Link` component from Next.js ```tsx import { NavItem } from '@keystone-6/core/admin-ui/components' ``` It expects the following props: ```ts type NavItemProps = { // The path or URL to navigate to href: string, // React children of the component children: ReactNode, // Toggles on the selected style and aria-current attribute of the NavItem isSelected: boolean } ``` By default the `isSelected` value will be evaluated by the condition `router.pathname === href`. Pass in `isSelected` if you have a custom condition or would like more granular control over the "selected" state of Navigation items. ## Related resources {% related-content %} {% well heading="Example: Custom Admin UI Navigation" href="https://github.com/keystonejs/keystone/tree/main/examples/custom-admin-ui-navigation" target="_blank" %} Adds a custom navigation component to the Admin UI. Builds on the Task Manager starter project. {% /well %} {% well heading="Custom Admin UI Logo Guide" href="/docs/guides/custom-admin-ui-logo" %} Learn how to add your own custom logo to Keystone’s Admin UI. {% /well %} {% well heading="Custom Admin UI Pages Guide" href="/docs/guides/custom-admin-ui-pages" %} Learn how to add your own custom pages to Keystone’s Admin UI. {% /well %} {% /related-content %} --- # System Configuration Source: https://keystonejs.com/docs/config/config The `keystone` [CLI](../guides/cli) expects to find a module named `keystone.ts` with a default export of a Keystone system configuration returned from the function `config()`. ```typescript filename=keystone.ts import { config } from '@keystone-6/core' export default config({ /* ... */ }) ``` The `config` function accepts an object representing all the configurable parts of the system: ```typescript export default config({ lists: { /* ... */ }, listDefaults: { /* ... */ }, db: { /* ... */ }, ui: { /* ... */ }, server: { /* ... */ }, session: { /* ... */ }, graphql: { /* ... */ }, extendGraphqlSchema: { /* ... */ }, }) ``` We will cover each of these options below. The configuration object has a TypeScript type of `KeystoneConfig`, which can be imported from `@keystone-6/core/types`. This type definition should be considered the source of truth for the available configuration options. Note: It is important to pass a `TypeInfo` type argument to the config function as it ensures proper typing for the [Keystone Context](../context/overview). This type is automatically created in `generated/keystone/types`. You can customize the output path of the generated type by specifying it in the config object. ```typescript import { TypeInfo } from './generated/keystone/types' export default config({ /* ... */ }) ``` ## lists The `lists` config option is where you define the data model, or schema, of the Keystone system. This is where you define and configure the `lists` and their `fields` of the data model. See the [Lists API](./lists) docs for details on how to use this function. ```typescript import { config } from '@keystone-6/core' import { TypeInfo } from './generated/keystone/types' export default config({ lists: { /* ... */ }, /* ... */ }) ``` ## listDefaults The `listDefaults` config option provides defaults for every list. It supports `graphql.omit` and `graphql.maxTake`: ```typescript export default config({ listDefaults: { graphql: { omit: { delete: true }, maxTake: 100, }, }, lists: { Post: list({ fields: { /* ... */ }, graphql: { omit: false, }, }), AuditLog: list({ fields: { /* ... */ }, graphql: { maxTake: Infinity, }, }), }, /* ... */ }); ``` Per-list configuration takes precedence. Boolean `omit` values replace the default entirely, while object values inherit operations that are not specified. An undefined `maxTake` inherits the default; set it to `Infinity` to make a list unlimited. ## db The `db` config option configures the database used to store data in your Keystone system. Keystone supports the database types **PostgreSQL**, **MySQL** and **SQLite**. These database types are powered by their corresponding Prisma database providers; `postgresql`, `mysql` and `sqlite`. - `provider`: The database provider to use, it can be one of `postgresql`, `mysql` or `sqlite`. - `prismaClientOptions`: A function returning the options for the Prisma client. This must include an `adapter` for your database. - `prismaClientPath` (default: `generated/prisma`): The output directory for the generated Prisma client. - `prismaSchemaPath` (default: `schema.prisma`): The path where Keystone writes its generated Prisma schema. - `extendPrismaClient`: A function that receives the generated Prisma Client instance and returns a client extended with custom functionality. - `extendPrismaSchema`: A function that receives the generated Prisma schema and returns a customised schema. - `onConnect`: which takes a [`KeystoneContext`](../context/overview) object, and lets perform any actions you might need at startup, such as data seeding - `idField` (default: `{ kind: "cuid" }`): The kind of id field to use, it can be one of: `cuid`, `uuid`, `nanoid`, `ulid`, or `autoincrement`. This can also be customised at the list level `db.idField`. If you are using `autoincrement`, you can also specify `type: 'BigInt'` on PostgreSQL and MySQL to use BigInts. The Database URL used by the Prisma CLI and `prisma db push` used in `keystone dev` are defined in the `prisma.config.ts`. Keystone creates an initial config if it is missing, but never overwrites the config. ### postgresql ```typescript import { PrismaPg } from '@prisma/adapter-pg' export default config({ db: { provider: 'postgresql', prismaClientOptions: () => ({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL! }), log: ['warn', 'error'], }), onConnect: async context => { /* ... */ }, idField: { kind: 'uuid' }, }, /* ... */ }) ``` ### mysql ```typescript import { PrismaMariaDb } from '@prisma/adapter-mariadb' export default config({ db: { provider: 'mysql', prismaClientOptions: () => ({ adapter: new PrismaMariaDb(process.env.DATABASE_URL!), }), onConnect: async context => { /* ... */ }, idField: { kind: 'uuid' }, }, /* ... */ }) ``` ### sqlite ```typescript import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), onConnect: async context => { /* ... */ }, idField: { kind: 'uuid' }, }, /* ... */ }) ``` #### Limitations The `sqlite` provider is not intended to be used in production systems, and has certain limitations: - `decimal`: The `decimal` field type is not supported. - `timestamp`: The `timestamp` field type only supports times within the range `1970 - 2038`. - `text`: The `text` field type does not support setting a filter as case sensitive or insensitive. Assuming default collation, all the filters except `contains`, `startsWith` and `endsWith` will be case sensitive and `contains`, `startsWith` and `endsWith` will be case insensitive but only for ASCII characters. - `select`: Using the `type: 'enum'`, the value will be represented as a string in the database. ## ui The `ui` config option configures the Admin UI which is provided by Keystone. This config option is for top level configuration of the Admin UI. Fine grained configuration of how lists and fields behave in the Admin UI is handled in the `lists` definition (see the [Lists API](./lists) for more details). Options: - `isDisabled` (default: `false`): If `isDisabled` is set to `true` then the Admin UI will be completely disabled. - `isAccessAllowed` (default: `(context) => context.session !== undefined`): This function controls whether a user can view the Admin UI. It takes a [`KeystoneContext`](../context/overview) object as an argument. Advanced configuration: - `publicPages` (default: `[]`): An array of page routes that bypass the `isAccessAllowed` function. - `pageMiddleware` (default: `undefined`): An async middleware function that can optionally return a redirect - `getAdditionalFiles` (default: `undefined`): An async function that returns an array of `AdminFileToWrite` objects indicating files to be added to the system at `build` time. If the `mode` is `'write'`, then the code to be written to the file should be provided as the `src` argument. If the `mode` is `'copy'` then an `inputPath` value should be provided. The `outputPath` indicates where the file should be written or copied to **Note**: This API is designed for use by plugins, such as the [`@keystone-6/auth`](./auth) package. See the [Custom Admin UI Pages](../guides/custom-admin-ui-pages) guide for details on simpler ways to customise your Admin UI. ```typescript export default config({ ui: { isDisabled: false, isAccessAllowed: async context => context.session !== undefined, // advanced configuration publicPages: ['/welcome'], getAdditionalFiles: async () => [ { mode: 'write', src: ` import { Heading } from '@keystar/ui/typography'; export default function Welcome() { return (

Welcome to my Keystone system

); }`, outputPath: 'pages/welcome.js', }, { mode: 'copy', inputPath: '...', outputPath: 'pages/farewell.js', }, ], }, /* ... */ }) ``` ## server The `dev` and `start` commands from the Keystone [command line](../guides/cli) will start an Express web-server for you. This server is configured via the `server` configuration option. Options: - `cors` (default: `undefined`): Allows you to configure the [cors middleware](https://www.npmjs.com/package/cors#configuration-options) for your Express server. If left undefined `cors` will not be used. - `port` (default: `3000` ): The port your Express server will listen on. - `options` (default: `undefined`): The [`http.createServer`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) options used by Node. - `maxFileSize` (default: `200 * 1024 * 1024`): The maximum file size allowed for uploads. If left undefined, defaults to `200 MiB` - `extendExpressApp` (default: `undefined`): Allows you to extend the express app that Keystone creates. - `extendHttpServer` (default: `undefined`): Allows you to extend the node `http` server that runs Keystone. ```typescript export default config({ server: { cors: { origin: ['http://localhost:7777'], credentials: true }, port: 3000, maxFileSize: 200 * 1024 * 1024, extendExpressApp: async (app, commonContext) => { /* ... */ }, extendHttpServer: async (httpServer, commonContext) => { /* ... */ }, }, /* ... */ }) ``` ### extendExpressApp This lets you modify the express app that Keystone creates _before_ the Apollo Server and Admin UI Middleware are added to it (but after the `cors` and `healthcheck` options are applied). The function is passed two arguments: - `app`: The express app keystone has created - `context`: A Keystone Context For example, you could add your own request logging middleware: ```ts export default config({ server: { extendExpressApp: app => { app.use((req, res, next) => { console.log('A request!') next() }) }, }, }) ``` Or add a custom route handler: ```ts export default config({ server: { extendExpressApp: app => { app.get('/_version', (req, res) => { res.send('v6.0.0-rc.2') }) }, }, }) ``` You could also use it to add custom REST endpoints to your server, by creating a context for the request and using the Query API Keystone provides: ```ts export default config({ server: { extendExpressApp: (app, commonContext) => { app.get('/api/users', async (req, res) => { const context = await commonContext.withRequest(req, res) const users = await context.query.User.findMany() res.json(users) }) }, }, }) ``` The created context will be bound to the request, including the current visitor's session, meaning access control will work the same as for GraphQL API requests. _ProTip!_: `extendExpressApp` can be `async` ### extendHttpServer This lets you interact with the node [http.Server](https://nodejs.org/api/http.html#class-httpserver) that Keystone uses. The function is passed in 3 arguments: - `server` - this is the HTTP server that you can then extend - `context`: A Keystone Context For example, this function could be used to listen for `'upgrade'` requests for a WebSocket server when adding support for GraphQL subscriptions ```ts import { WebSocketServer } from 'ws' import { useServer as wsUseServer } from 'graphql-ws/lib/use/ws' export default config({ server: { extendHttpServer: (httpServer, commonContext) => { const wss = new WebSocketServer({ server: httpServer, path: '/api/graphql', }) wsUseServer({ schema: commonContext.graphql.schema }, wss) }, }, }) ``` _Note_: when using `keystone dev`, `extendHttpServer` is only called once on startup - you will need to restart your process for any updates ## session The `session` config option allows you to configure session management of your Keystone system. In general you will use `SessionStrategy` objects from the `@keystone-6/core/session` package, rather than writing this yourself. ```typescript import { statelessSessions } from '@keystone-6/core/session' export default config({ session: statelessSessions({ /* ... */ }), /* ... */ }) ``` See the [Session API](./session) for more details on how to configure session management in Keystone. ## graphql The `graphql` config option allows you to configure certain aspects of your GraphQL API. Options: - `debug` (default: `process.env.NODE_ENV !== 'production'`): If `true`, stacktraces from both Apollo errors and Keystone errors will be included in the errors returned from the GraphQL API. These can be filtered out with `apolloConfig.formatError` if you need to process them, but do not want them returned over the GraphQL API. - `path` (default: `'/api/graphql'`): The path of the GraphQL API endpoint. - `playground` (default: `process.env.NODE_ENV !== 'production'`) - `true` - Add `ApolloServerPluginLandingPageGraphQLPlayground` to the Apollo Server plugins - `false` - Add `ApolloServerPluginLandingPageDisabled` to the Apollo Server plugins - `'apollo'` - Do not add any plugins to the Apollo config, this will use [Apollo Sandbox](https://www.apollographql.com/docs/apollo-server/testing/build-run-queries/#apollo-sandbox) - `apolloConfig` (default: `undefined`): Allows you to pass [extra options](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#constructor) into the `ApolloServer` constructor. - `schemaPath` (default: `schema.graphql`): The path of the generated GraphQL API schema. ```typescript export default config({ graphql: { debug: process.env.NODE_ENV !== 'production', path: '/api/graphql', apolloConfig: { debug: true, /* ... */ }, }, /* ... */ }) ``` ## extendGraphqlSchema The `extendGraphqlSchema` config option allows you to extend the GraphQL API which is generated by Keystone based on your schema definition. It has a TypeScript type of `(schema: import("graphql").GraphQLSchema) => import("graphql").GraphQLSchema`. `extendGraphqlSchema` expects a function that takes the GraphQL Schema generated by Keystone and returns a valid GraphQL Schema ```typescript import type { GraphQLSchema } from 'graphql' import { config, g } from '@keystone-6/core' export default config({ extendGraphqlSchema: (keystoneSchema: GraphQLSchema) => { /* ... */ return newExtendedSchema }, /* ... */ }) ``` See the [schema extension guide](../guides/schema-extension) for more details and tooling options on how to extend your GraphQL API. ## OpenTelemetry Keystone has built-in support for [OpenTelemetry](https://opentelemetry.io/) tracing via `@opentelemetry/api`. When you configure an OpenTelemetry SDK in your application, Keystone will automatically emit trace spans for its internal operations (such as CRUD resolvers and hooks). No additional Keystone configuration is required — just set up the OpenTelemetry SDK before Keystone starts. See the `logging-opentelemetry` example for a complete setup. ## Related resources {% related-content %} {% well heading="Lists API Reference" href="/docs/config/lists" %} The API to configure your options used with the `list` function. {% /well %} {% /related-content %} --- # Lists API Source: https://keystonejs.com/docs/config/lists The `lists` property of the [system configuration](./config) object is where you define the data model, or schema, of your Keystone system. It accepts an object with list names as keys, and `list()` configurations as values. ```typescript import { config, list } from '@keystone-6/core' export default config({ lists: { SomeListName: list({ fields: { /* ... */ }, actions: { /* ... */ }, access: { /* ... */ }, ui: { /* ... */ }, hooks: { /* ... */ }, graphql: { /* ... */ }, db: { /* ... */ }, isSingleton: false, }), /* ... */ }, /* ... */ }) ``` This document will explain the configuration options which can be used with the `list()` function. Options: - `isSingleton`: This flag, when `true` changes the list to default to only supporting a single row. See [Singletons](#is-singleton) for details. ## fields The `fields` option defines the names, types, and configuration of the fields in the list. This configuration option takes an object with field names as keys and configured field types as values. ```typescript import { config, list } from '@keystone-6/core' import { text } from '@keystone-6/core/fields' export default config({ lists: { SomeListName: list({ fields: { someFieldName: text({ /* ... */ }), /* ... */ }, }), /* ... */ }, /* ... */ }) ``` For full details on the available field types and their configuration options please see the [Fields API](../fields/overview). ## actions The `actions` property of the list configuration object is where you define actions that can triggered for items on your list. An action can be triggered on individual items or in bulk from the list view in the Admin UI, or directly using GraphQL. ```typescript import { config, list, action } from '@keystone-6/core' import { allowAll } from '@keystone-6/core/access' import { text, integer } from '@keystone-6/core/fields' export default config({ lists: { Post: list({ access: allowAll, fields: { title: text(), votes: integer({ defaultValue: 0 }), }, actions: { vote: action({ access: allowAll, async resolve({ where }, context) { if (!where) return null return await context.prisma.post.update({ where: { id: where.id }, data: { votes: { increment: 1 } }, }) }, ui: { label: 'Vote +1', icon: 'voteIcon', }, }), }, }), }, }) ``` Each action accepts the following options: - `access` (**required**): An access control function that determines who can use this action. Receives `{ context, session, listKey, actionKey }`. - `resolve` (**required**): The function that performs the action. Receives `{ listKey, actionKey, where }` and `context`. Should return the updated item or `null`. - `graphql`: Options for the GraphQL schema output. - `singular`: Override the name of the singular mutation (default: `{action}{list.graphql.singular}`, e.g. `votePost`). - `plural`: Override the name of the plural mutation (default: `{action}{list.graphql.plural}`, e.g. `votePosts`). - `description` (default: `undefined`): Sets the description of the associated GraphQL type in the GraphQL schema output. - `ui` (**required**): Controls how the action appears in the Admin UI. - `label` (**required**): The label shown on the action button. - `icon`: An icon name from `@keystar/ui/icon/all` to display alongside the label. - `messages`: Locale messages for prompts and notifications. Supports template tags: `{singular}`, `{plural}`, `{singular|plural}`, `{itemLabel}`, `{count}`, `{countSuccess}`, `{countFail}`. - `promptTitle`, `promptTitleMany`, `prompt`, `promptMany`: Confirmation dialog text. - `promptConfirmLabel`, `promptConfirmLabelMany`: Confirm button text. - `success`, `successMany`: Success toast messages. - `fail`, `failMany`: Failure toast messages. - `itemView`: Controls for the item view. - `actionMode` (default: `'enabled'`): Can be `'enabled'`, `'disabled'`, or `'hidden'`, a conditional filter object, or a function that returns one of those values. Conditional filter objects can combine field predicates with nested `AND`, `OR`, and `NOT` groups. - `navigation` (default: `'follow'`): Controls navigation after the action completes. `'follow'` navigates to the returned item (or list view if `null`), `'refetch'` stays and refreshes the item, `'return'` goes back to the list view. - `hidePrompt` (default: `false`): Do not show a confirmation dialog. - `hideToast` (default: `false`): Do not show a toast notification. - `listView`: Controls for the list view. - `actionMode` (default: `'enabled'`): Can be `'enabled'` or `'hidden'`, a conditional filter object, or a function that returns one of those values. Conditional filter objects can combine field predicates with nested `AND`, `OR`, and `NOT` groups. ## access The `access` option defines the [Access Control](../guides/auth-and-access-control) rules for the list. These rules determine which of the CRUD (create, read, update, delete) operations users are allowed to perform. See the [Access Control API](./access-control) for full details on the available access control options. ## fieldDefaults The `fieldDefaults` option provides defaults using the same nesting as field configuration. It supports `access`, `graphql.omit` and `ui.createView/itemView/listView.fieldMode`. Per-field configuration takes precedence; group UI defaults take precedence over list UI defaults. ## ui The `ui` option controls how the list is displayed and interacted with in the Admin UI. Options: - `labelField`: Selects the field which will be used as the label column in the Admin UI. By default looks for a field called `'label'`, then falls back to `'name'`, then `'title'`, and finally `'id'`, which is guaranteed to exist. - `searchFields`: The fields used by the Admin UI when searching this list on the list view and in relationship fields. Nominated fields need to support the `contains` filter. It is always possible to search by an id and `'id'` should not be specified in this option. By default, the `labelField` is used if it has a string `contains` filter, otherwise none. - `hideNavigation` (default: `false`): Controls whether the list is visible in the navigation elements of the Admin UI. Can be either a boolean value or an async function with an argument `{ session, context }` that returns a boolean value. - `hideCreate` (default: `false`): Controls whether the `create` button is available in the Admin UI for this list. Can be either a boolean value or an async function with an argument `{ session, context }` that returns a boolean value. - `hideDelete` (default: `false`): Controls whether the `delete` button is available in the Admin UI for this list. Can be either a boolean value or an async function with an argument `{ session, context }` that returns a boolean value. - `listView`: Controls the list view page of the Admin UI. - `initialColumns` (default: The first three fields defined in the list). A list of field names to display in columns in the list view. By default only the label column, as determined by `labelField`, is shown. - `initialSort` (default: `undefined`): Sets the field and direction to be used to initially sort the data in the list view. Option `field` is the name of the field to sort by, and `direction` is either `'ASC'` or `'DESC'` for ascending and descending sorting respectively. If undefined then data will be unsorted. - `pageSize` (default: lower of `50` or [`graphql.maxTake`](#graphql)): Sets the number of items to show per page in the list view. - `initialFilter` (default: `undefined`): Sets a default column filter to apply to the list view. Accepts a where input object (excluding `AND`, `OR`, `NOT`), or an async function with an argument `{ session, context }` that returns a where input object. - `label`: The label used to identify the list in navigation etc. - `singular`: The singular form of the list key. It is used in sentences like `Are you sure you want to delete this {singular}?` - `plural`: The plural form of the list key. It is used in sentences like `Are you sure you want to delete these {plural}?` - `path`: A path segment to identify the list in URLs. It must match the pattern `/^[a-z-_][a-z0-9-_]*$/`. ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { name: text({ /* ... */ }) }, ui: { label: 'Some List', singular: 'Item', plural: 'Items', path: 'some-list', labelField: 'name', searchFields: ['name', 'alternativeName'], hideNavigation: ({ session, context }) => false, hideCreate: ({ session, context }) => false, hideDelete: ({ session, context }) => false, listView: { initialColumns: ['name', /* ... */], initialSort: { field: 'name', direction: 'ASC' }, initialFilter: { isPublished: { equals: true } }, pageSize: 50, }, }, fieldDefaults: { ui: { createView: { fieldMode: ({ session, context }) => 'edit' }, itemView: { fieldMode: ({ session, context, item }) => 'edit' }, listView: { fieldMode: ({ session, context }) => 'read' }, }, }, }), /* ... */ }, /* ... */ }); ``` ## hooks The `hooks` option defines [hook](../guides/hooks) functions for the list. Hooks allow you to execute code at different stages of the mutation lifecycle. See the [Hooks API](./hooks) for full details on the available hook options. ## graphql The `graphql` property allows you to configure certain aspects of the GraphQL API. Options: - `description` (default: `undefined`): Sets the description of the associated GraphQL type in the GraphQL schema output. - `singular`: (default: Singular list key, e.g. `'User'`): Overrides the name used in singular mutations and queries (e.g. `user()`, `updateUser()`, etc). - `plural`: (default: Pluralised list key, e.g. `'Users'`): Overrides the name used in multiple mutations and queries (e.g. `users()`, `updateUsers()`, etc). - `maxTake` (default: `undefined`): Allows you to specify the maximum `take` number for query operations on this list in the GraphQL API. - `cacheHint` (default: `undefined`): Allows you to specify the [dynamic cache control hints](https://www.apollographql.com/docs/apollo-server/performance/caching/#in-your-resolvers-dynamic) used for queries to this list. - `omit` (default: `undefined`): Allows you to configure which parts of the CRUD API are autogenerated for your GraphQL API. This option accepts either `true`, or an object with the below fields. If you specify `true` then the entire list, including its output type, will be omitted from the GraphQL API. - `omit.query` (default: `false`): A boolean omits all queries, or use an object with required `one`, `many`, and `count` booleans. These settings also control singular relationships, many relationships, and relationship counts respectively. - `omit.create` (default: `false`): If set to true, the create mutation will be omitted from the GraphQL API for this list. - `omit.update` (default: `false`): If set to true, the update mutation will be omitted from the GraphQL API for this list. - `omit.delete` (default: `false`): If set to true, the delete mutation will be omitted from the GraphQL API for this list. ```typescript import { config, list } from '@keystone-6/core' export default config({ lists: { SomeListName: list({ graphql: { description: '...', singular: '...', plural: '...', maxTake: 100, cacheHint: { maxAge: 60, scope: 'PRIVATE' }, omit: { query: { one: true, many: true, count: true }, create: true, update: true, delete: true, }, }, /* ... */ }), /* ... */ }, /* ... */ }) ``` ## db The `db` config option allows you to configure certain aspects of the database connection specific to this list. Options: - `idField` (default: `{ kind: 'cuid' }`): The kind of id field to use, it can be one of: `cuid`, `uuid` or `autoincrement`. The default across all lists can be changed at the root-level `db.idField` config. If you are using `autoincrement`, you can also specify `type: 'BigInt'` on PostgreSQL and MySQL to use BigInts. - `map`: Adds a [Prisma `@@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map-1) attribute to the Prisma model for this list which specifies a custom database table name for the list, instead of using the list key ```typescript import { config, list } from '@keystone-6/core' export default config({ lists: { SomeListName: list({ db: { idField: { kind: 'uuid' }, map: 'table_name', }, /* ... */ }), /* ... */ }, /* ... */ }) ``` ## isSingleton The `isSingleton` flag changes a list to only have support for a single row with an `id` of `1`. With the flag set, when an item is created it is given an `id` of `1`, and when an item is queried from a list, the GraphQL `where` filter defaults to `{ id: '1' }`. An example of when this might helpful is for editing data like your frontend configuration, when it wouldn't otherwise be checked into source control. Abstracting singletons as a behavioural trait of lists instead of a distinct type helps developers build functions for lists without needing to know the underlying constraints, effectively ensuring that lists remain as functors. Using GraphQL, to query a list named `seoConfiguration`, with `isSingleton` set, you can write any of the following queries ```graphql query { # singular (null or an item) seoConfiguration { title description } # plural (0 or 1 items) seoConfigurations { title description } } ``` In the Admin UI, lists with `isSingleton` set do not have a list view, instead redirecting you to the item view page of the item with an `id` of `1`. The following additional constraints should be kept in mind when lists that have `isSingleton` set — - With `id: 1` injected into respective filters, the `id` unique constraint will fail for create operations if an item already exists - You cannot have relationships (`ref: 'Settings'`), if `Settings` is a list with `isSingleton` set - You can however, have relationship fields in the `Settings` list, like normal Try out our [Singleton Example](https://github.com/keystonejs/keystone/blob/main/examples/singleton/schema.ts) to see it in action. ## Related resources {% related-content %} {% well heading="Fields API Reference" href="/docs/fields/overview" %} Defines the names, types, and configuration of Keystone fields. See all the fields and the configuration options they accept. {% /well %} {% well heading="Config API Reference" href="/docs/config/overview" %} The API to configure all the parts of your Keystone system. {% /well %} {% well heading="Example Project: Blog" href="https://github.com/keystonejs/keystone/tree/main/examples/usecase-blog" target="_blank" %} A basic Blog schema with Posts and Authors. Use this as a starting place for learning how to use Keystone. It’s also a starter for other feature projects. {% /well %} {% /related-content %} --- # Authentication Source: https://keystonejs.com/docs/config/auth Keystone allows you to extend your Keystone system to support authentication against a `password` field using the `createAuth()` function in the `@keystone-6/auth` package. For examples of how to use authentication in your system please see the [authentication guide](../guides/auth-and-access-control). ```typescript import { config, list } from '@keystone-6/core' import { text, password, checkbox } from '@keystone-6/core/fields' import { createAuth } from '@keystone-6/auth' const { withAuth } = createAuth({ // Required options listKey: 'User', identityField: 'email', secretField: 'password', // Additional options sessionData: 'id email', }) export default withAuth( config({ lists: { User: list({ fields: { email: text({ isIndexed: 'unique' }), password: password(), isAdmin: checkbox(), }, }), }, session: { /* ... */ }, }) ) ``` The function `createAuth` returns a function `withAuth` which should be used to wrap your `config()`. This wrapper function will modify the config object to inject extra fields, extra GraphQL queries and mutations, and custom Admin UI functionality into the system. The `createAuth` function must be used in conjunction with a [session](./session) configuration. ## Required options The core functionality of the authentication system provides a GraphQL mutation to authenticate a user and then start a session, and a sign in page in the Admin UI. - `listKey`: The name of the list to authenticate against. - `identityField`: The name of the field to use as an identity field. This field must have `{ isIndexed: 'unique' }` set. - `secretField`: The name of the field to use as a secret. This field must be a `password()` field type. ```typescript import { createAuth } from '@keystone-6/auth' const { withAuth } = createAuth({ listKey: 'User', identityField: 'email', secretField: 'password', }) ``` #### GraphQL API {% #graphql-api %} The following elements will be added to the GraphQL API. ```graphql type Mutation { authenticateUserWithPassword( email: String! password: String! ): UserAuthenticationWithPasswordResult! } type Query { authenticatedItem: AuthenticatedItem } union AuthenticatedItem = User union UserAuthenticationWithPasswordResult = | UserAuthenticationWithPasswordSuccess | UserAuthenticationWithPasswordFailure type UserAuthenticationWithPasswordSuccess { sessionToken: String! item: User! } type UserAuthenticationWithPasswordFailure { message: String! } ``` ##### authenticateUserWithPassword This mutation will check the supplied credentials and start a new [session](./session) if the credentials are valid. The argument names for this function are the values of `identityField` and `secretField`. ```graphql mutation { authenticateUserWithPassword(email: "username@example.com", password: "password") { ... on UserAuthenticationWithPasswordSuccess { item { id email } } ... on UserAuthenticationWithPasswordFailure { message } } } ``` On success the session handler will start a new session and return the encoded session cookie data as `sessionToken`. The authenticated item will be returned as `item`. On failure the values `{ code: FAILURE, message: "Authentication failed." }` will be returned. ##### authenticatedItem This query will return the currently logged in user, based on the `session` data. #### Admin UI A sign in page at the path `/signin` will be added to the Admin UI. If a user tries to access the Admin UI without having logged in they will be redirected back to `/signin`. This page uses the `authenticateUserWithPassword` mutation to let users sign in to the Admin UI. ## Additional options The following options add extra functionality to your Keystone authentication system. By default they are disabled. ### sessionData This option adds support for setting a custom `session.data` value based on the authenticated user. The authentication mutations will set the values `{ listKey, itemId }` on the `context.session` object. You will often need to know more than just the `itemId` of the authenticated user, such as when performing [access-control](../guides/auth-and-access-control) or using [hooks](../guides/hooks). Configuring `sessionData` will add an `session.data` based on the `itemId`, populated by the fields given in `sessionData.query`. The value is a GraphQL query string which indicates which fields should be populated on the `session.data` object ```typescript import { createAuth } from '@keystone-6/auth' const { withAuth } = createAuth({ listKey: 'User', identityField: 'email', secretField: 'password', sessionData: 'id name isAdmin', }) ``` ## Related resources {% related-content %} {% well heading="Example Project: Authentication" href="https://github.com/keystonejs/keystone/tree/main/examples/auth" %} Adds password-based authentication to the Task Manager starter project. {% /well %} {% /related-content %} --- # Access Control Source: https://keystonejs.com/docs/config/access-control {% hint kind="warn" %} We recently improved this API so it’s easier to program, and makes it harder to introduce security gaps in your system. If you were using it prior to September 1st 2022, you will need to now configure access control for your operations. {% /hint %} The `access` property of the [list configuration](./lists) object configures who can query, create, update, and delete items in your Keystone system. The `access` property of the [field configuration](../fields/overview) object configures who can read, create, and update specific field values of an item. ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { ListKey: list({ fields: { fieldName: text({ access: { /* ... */ }, }), }, access: { /* ... */ }, }), }, }); ``` ## List Access Control Keystone provides support for access control and filtering on a per-list basis. There are three types of access control that can be configured on the `access` object: - [`operation`](#operation), functions that provide the `context` and `session` to help decide if a request should be granted access to the list; this happens prior to any GraphQL query being executed. - [`filter`](#filter), functions which return a [filter](../graphql/filters) that is applied to database queries on this list. - [`item`](#item-mutations-only), functions that provide the `context` and `session` to determine if a request can execute the respective mutative action. Each of these types of access control are applied before [hooks](./hooks). {% hint kind="warn" %} Please note that Keystone does not automatically configure nor leverage database row security policies or row level security {% /hint %} If you are happy to allow all parts of a list to be accessible you can use: ```typescript import { config, list } from '@keystone-6/core'; import { allowAll } from '@keystone-6/core/access'; export default config({ lists: { ListKey: list({ access: allowAll, }), }, }); ``` {% hint kind="tip" %} **Access denied:** **Mutations** return `null` data with an access denied error. **Queries** never return access denied errors, and instead treat items as if they didn't exist. {% /hint %} If access is **denied** due to any of the access control methods, the following responses will be returned from the GraphQL API: - **Mutations** - **Single** operations return `null` and an access denied error. - **Multi** operations return a data array with `null` values for the items which have access denied. Error responses are returned for each `null` item. - **Queries** - **Single** item queries return `null` with no errors. - **Many** item queries filter out items which have access denied. No error responses are returned. - **Count** queries will only count those items for which access is **not** denied. No error responses are returned. ### Operation Operation-level access control lets you control which operations can be accessed by a user based on the `session` and `context`. Individual functions can be provided for each of the operations. All operations must be configured. {% hint kind="tip" %} These functions must return `true` if the operation is allowed, or `false` if it is not allowed. {% /hint %} ```typescript import { config, list } from '@keystone-6/core'; export default config({ lists: { ListKey: list({ access: { operation: { query: { one: ({ session, context, listKey, operation, kind }) => true, many: ({ session, context, listKey, operation, kind }) => true, count: ({ session, context, listKey, operation, kind }) => true, }, create: ({ session, context, listKey, operation }) => true, update: ({ session, context, listKey, operation }) => true, delete: ({ session, context, listKey, operation }) => true, } }, }), }, }); ``` If you are happy setting all of them to true, you can set it for all operations with: ```typescript import { config, list } from '@keystone-6/core'; import { allowAll } from '@keystone-6/core/access'; export default config({ lists: { ListKey: list({ access: { operation: allowAll }, }), }, }); ``` If you want to set all but one operation to true, you can do so using `allOperations` and `denyAll` from `@keystone-6/core/access` such as: ```typescript import { config, list } from '@keystone-6/core'; import { denyAll, allOperations } from '@keystone-6/core/access'; export default config({ lists: { ListKey: list({ access: { operation: { ...allOperations(denyAll), // hint: unconditionally returning `true` is equivalent to using allowAll for this operation query: ({ session, context, listKey, operation, kind }) => true, } }, }), }, }); ``` {% hint kind="warn" %} The `query` access control is applied only when running GraphQL query operations. It can be a shared function receiving `kind: 'one' | 'many' | 'count'`, or an object with required `one`, `many`, and `count` functions. A user can still **read** fields as part of a return query when using a mutation `operation` (`create`, `update` or `delete`). If you want to limit access to fields, use [field access control](https://keystonejs.com/docs/config/access-control#field-access-control). {% /hint %} ### Filter Filter-level access control lets you restrict which items can be operated on by providing a function which returns a [GraphQL filter](../graphql/filters). - For **mutations**, the access control filter will be combined with the unique identifier provided to the operation, and access will be denied if no item is found with this combined filter. - For **queries**, the access control filter will be combined with the query filter and only items which match both filters will be returned. In general, the filter access control functions will return GraphQL filters. They can also return boolean values `true` or `false` to match or exclude all items. {% hint kind="warn" %} Each `filter` type _defaults to an empty filter_, unless configured. {% /hint %} ```typescript import { config, list } from '@keystone-6/core'; import { checkbox } from '@keystone-6/core/fields'; export default config({ lists: { ListKey: list({ fields: { isReadable: checkbox(), isUpdatable: checkbox(), isDeletable: checkbox(), } access: { filter: { query: ({ session, context, listKey, operation }) => { return { isReadable: { equals: true } }; }, update: ({ session, context, listKey, operation }) => { return { isUpdatable: { equals: true } }; }, delete: ({ session, context, listKey, operation }) => { return { isDeletable: { equals: true } }; }, } }, }), }, }); ``` {% hint kind="tip" %} Filter based access control cannot be used for `create` operations. If you want to limit `create` operations, use either `access.operation.create` or `access.item.create`. {% /hint %} ### Item (mutations only) Item-level access control lets you control which mutative operations can be applied to a list item. Individual functions can be provided for each of the operations, with each function receiving: - the **input data** of the mutation (for `create` and `update` operations), and/or - the **existing item** in the database (for `update` and `delete` operations). {% hint kind="warn" %} Each `item` access type _defaults to public_ unless configured. {% /hint %} {% hint kind="tip" %} These functions must return `true` if the operation is allowed, or `false` if it is not allowed. {% /hint %} ```typescript import { config, list } from '@keystone-6/core'; import { checkbox } from '@keystone-6/core/fields'; export default config({ lists: { ListKey: list({ access: { item: { create: ({ session, context, listKey, operation, inputData }) => true, update: ({ session, context, listKey, operation, inputData, item }) => true, delete: ({ session, context, listKey, operation, item }) => true, } }, }), }, }); ``` {% hint kind="warn" %} Item-level access control is not available for `query` operations. Keystone has opted to disable `read` access control for `query` operations as applying _after_ queries will reuslt in inconsistent pagination behaviour and mismatched `count` results. {% /hint %} ### Function Arguments {% #list-level-function-arguments %} List-level access control functions are passed a collection of arguments which can be used to determine whether the operation is allowed. | Argument | Description | | ----------- | ------------------------------------------------------------------------------------------------------------- | | `session` | The current session object. See the [Sessions API](./session) for details. | | `context` | The [`KeystoneContext`](../context/overview) object of the originating GraphQL operation. | | `listKey` | The key of the list being operated on. | | `operation` | The operation being performed (`'query'`, `'create'`, `'update'`, `'delete'`). | | `kind` | For operation access on `query`, the query kind (`'one'`, `'many'`, or `'count'`). | | `inputData` | For `create` and `update` operations, this is the value of `data` passed into the mutation. (Item level only) | | `item` | The existing item being updated/deleted in `update` and `delete` operations. (Item level only) | ## Field Access Control Keystone also allows you to set up access control on a per-field basis. Rules can be set for `read`, `create` and `update` operations. {% hint kind="tip" %} Each operation is defined by a function which returns `true` if access is allowed and `false` if access is not allowed. {% /hint %} ### Mutations Field-level access control rules are applied **after** the list level access rules have been applied. Access control rules are only applied to the fields that have an input value provided to the mutation. If any of the provided fields fail their access control check, the whole operation is aborted. The GraphQL API then returns `null` along with an access denied error. ### Read Field-level access control rules are applied when trying to resolve a field on the output type. If access is denied then the query will still return an item object, but the specific field will return `null`. No errors will be returned for `read` access denied. {% hint kind="tip" %} The `read` access control is applied to fields returned from both **queries** and **mutations**. {% /hint %} `read` may be a shared function receiving `kind: 'item' | 'filter' | 'order'`, or an object with all three functions. The `item` argument is only present for the `item` kind. {% hint kind="tip" %} `read` access control is applied as part of GraphQL resolving the output types. If a mutation returns an item type that has field access control defined, field access control will apply. {% /hint %} {% hint kind="warn" %} `read` field access control does not apply to `context.db.*` operations, as these operations do not resolve the underlying fields using GraphQL. {% /hint %} ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { ListKey: list({ fields: { fieldName: text({ access: { read: { item: ({ session, context, listKey, fieldKey, operation, kind, item }) => true, filter: ({ session, context, listKey, fieldKey, operation, kind }) => true, order: ({ session, context, listKey, fieldKey, operation, kind }) => true, }, create: ({ session, context, listKey, fieldKey, operation, inputData }) => true, update: ({ session, context, listKey, fieldKey, operation, inputData, item }) => true, }, }), }, }), }, }); ``` {% hint kind="tip" %} Field-level access control is not available for `delete` operations. Restrict `delete` operations at the List-level instead with `access.operation.delete`, `access.filter.delete` or `access.item.delete`. {% /hint %} ### Function Arguments {% #field-level-function-arguments %} Field-level access control functions are passed a collection of arguments which can be used to determine whether the operation is allowed. | Argument | Description | | ----------- | ------------------------------------------------------------------------------------------- | | `session` | The current session object. See the [Sessions API](./session) for details. | | `context` | The [`KeystoneContext`](../context/overview) object of the originating GraphQL operation. | | `listKey` | The key of the list being operated on. | | `fieldKey` | The key of the field being operated on. | | `operation` | The operation being performed (`'read'`, `'create'`, `'update'`). | | `kind` | For `read`, the kind being checked (`'item'`, `'filter'`, or `'order'`). | | `inputData` | For `create` and `update` operations, this is the value of `data` passed into the mutation. | | `item` | The existing item. For `read`, this is present only when `kind` is `'item'`. | --- # Hooks Source: https://keystonejs.com/docs/config/hooks Hooks allow you to execute code at different stages of the mutation lifecycle when performing create, update, and delete operations. Lists and fields both support the same set of hook functions, with some slight differences in the arguments they accept. The differences will be explicitly called out below. For each hook, the fields hooks are applied to **all fields first** in parallel, followed by the list hooks. Hook functions support `async` and, with the exception of `resolveInput`, do not need a return value. When operating on multiple values the hooks are called individually for each item being updated, created or deleted. For examples of how to use hooks in your system please see the [hooks guide](../guides/hooks). ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ hooks: { resolveInput: { create: async args => { /* ... */ }, update: async args => { /* ... */ }, }, validate: { create: async args => { /* ... */ }, update: async args => { /* ... */ }, delete: async args => { /* ... */ }, }, beforeOperation: { create: async args => { /* ... */ }, update: async args => { /* ... */ }, delete: async args => { /* ... */ }, }, afterOperation: { create: async args => { /* ... */ }, update: async args => { /* ... */ }, delete: async args => { /* ... */ }, } }, fields: { someFieldName: text({ hooks: { resolveInput: { create: async args => { /* ... */ }, update: async args => { /* ... */ }, }, validate: { create: async args => { /* ... */ }, update: async args => { /* ... */ }, delete: async args => { /* ... */ }, }, beforeOperation: { create: async args => { /* ... */ }, update: async args => { /* ... */ }, delete: async args => { /* ... */ }, }, afterOperation: { create: async args => { /* ... */ }, update: async args => { /* ... */ }, delete: async args => { /* ... */ }, } }, }), }, }), }, }); ``` ### resolveInput The `resolveInput` hook is a transform for mutating the input `data` value prior to calling any other successive hooks, as part of the operation. This hook is the final stage in the [data resolving process](#resolved-data-stages), and is invoked after access control has been applied. For field hooks, the return value should be an updated value for that specific field. For list hooks, the return value should be a [`resolved data`](#resolved-data-stages) object. The result of `resolveInput` hooks is accessible as the argument `resolvedData` in the hooks that follow, for the remainder of the operation. | Argument | Description | | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `listKey` | The key of the list being operated on. | | `fieldKey` | The key of the field being operated on (field hooks only). | | `operation` | The operation being performed (`'create'` or `'update'`). | | `inputData` | The value of `data` passed into the mutation. | | `inputFieldData` | The value of the field from the input data (field hooks only). | | `item` | The currently stored item (`undefined` for `create` operations). This object is an internal database item. [DB API](../context/db-items) for more details on internal database items. | | `itemField` | The value of the field from the current item (field hooks only, `undefined` for `create` operations). | | `resolvedData` | A [`resolved data`](#resolved-data-stages) object. The resolved data value after default values, relationship resolvers, field resolvers, and `resolveInput` hooks have been applied. | | `resolvedFieldData` | The resolved value for this specific field (field hooks only). | | `context` | The [`KeystoneContext`](../context/overview) object of the originating GraphQL operation. | ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ hooks: { resolveInput: { create: async ({ listKey, operation, // always 'create' inputData, item, resolvedData, context, }) => { /* ... */ return resolvedData; }, update: async ({ listKey, operation, // always 'update' inputData, item, resolvedData, context, }) => { /* ... */ return resolvedData; }, }, }, fields: { someFieldName: text({ hooks: { resolveInput: { create: async ({ listKey, fieldKey, operation, inputData, inputFieldData, item, itemField, resolvedData, resolvedFieldData, context, }) => { /* ... */ return resolvedData[fieldKey]; }, update: async ({ listKey, fieldKey, operation, inputData, inputFieldData, item, itemField, resolvedData, resolvedFieldData, context, }) => { /* ... */ return resolvedData[fieldKey]; }, }, }, }), }, }), }, }); ``` ### validate The `validate` hooks can be used to validate your [`resolvedData`](#resolved-data-stages) before a `create` or `update` operation completes, ensuring your expectations are met. This hook can additionally be used to check your expectations as part of a `delete` operation. For `create` and `update` operations, this hook is invoked after the respective `resolveInput` hooks has been run. This hook should report any validation errors using the `addValidationError(message)` function, which is provided as a parameter. This is preferred to throwing to easily support more than one error message, if required. These error messages will be returned as a `ValidationFailureError` from the GraphQL API, and the operation will not be completed. | Argument | Description | | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `listKey` | The key of the list being operated on. | | `fieldKey` | The key of the field being operated on (field hooks only). | | `operation` | The operation being performed (`'create'`, `'update'` or `'delete'`). | | `inputData` | The value of `data` passed into the mutation (`undefined` for `delete` operations). | | `inputFieldData` | The value of the field from the input data (field hooks only, `undefined` for `delete` operations). | | `item` | The current value of the item being updated (`undefined` for `create` operations). This object is an internal database item. [DB API](../context/db-items) for more details on internal database items. | | `itemField` | The value of the field from the current item (field hooks only, `undefined` for `create` operations). | | `resolvedData` | A [`resolved data`](#resolved-data-stages) object (`undefined` for `delete` operations). The resolved data value after all data resolver stages have been completed. | | `resolvedFieldData` | The resolved value for this specific field (field hooks only, `undefined` for `delete` operations). | | `context` | The [`KeystoneContext`](../context/overview) object of the originating GraphQL operation. | | `addValidationError(msg)` | Used to set a validation error. | ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ hooks: { validate: { create: async ({ listKey, operation, inputData, resolvedData, context, addValidationError, }) => { /* ... */ }, update: async ({ listKey, operation, inputData, item, resolvedData, context, addValidationError, }) => { /* ... */ }, delete: async ({ listKey, operation, item, context, addValidationError, }) => { /* ... */ }, }, }, fields: { someFieldName: text({ hooks: { validate: { create: async ({ listKey, fieldKey, operation, inputData, inputFieldData, resolvedData, resolvedFieldData, context, addValidationError, }) => { /* ... */ }, update: async ({ listKey, fieldKey, operation, inputData, inputFieldData, item, itemField, resolvedData, resolvedFieldData, context, addValidationError, }) => { /* ... */ }, delete: async ({ listKey, fieldKey, operation, item, itemField, context, addValidationError, }) => { /* ... */ }, }, }, }), }, }), }, }); ``` ### beforeOperation The `beforeOperation` hook is used to perform side effects just before the data is saved to the database (for a `create` or `update` operation), or deleted from the database (for `delete` operations). It is invoked after the `resolveInput` and `validate` hooks, but before the database is updated by Prisma. | Argument | Description | | :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `listKey` | The key of the list being operated on. | | `fieldKey` | The key of the field being operated on (field hooks only). | | `operation` | The operation being performed (`'create'`, `'update'`, or `'delete'`). | | `inputData` | The value of `data` passed into the mutation. `undefined` for `delete` operations. | | `inputFieldData` | The value of the field from the input data (field hooks only, `undefined` for `delete` operations). | | `item` | The current value of the item being updated, `undefined` for `create` operations. This object is an internal database item. [DB API](../context/db-items) for more details on internal database items. | | `itemField` | The value of the field from the current item (field hooks only, `undefined` for `create` operations). | | `resolvedData` | A [`resolved data`](#resolved-data-stages) object. The resolved data value after all data resolver stages have been completed. `undefined` for `delete` operations. | | `resolvedFieldData` | The resolved value for this specific field (field hooks only, `undefined` for `delete` operations). | | `context` | The [`KeystoneContext`](../context/overview) object of the originating GraphQL operation. | ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ hooks: { beforeOperation: { create: async ({ listKey, operation, inputData, resolvedData, context, }) => { /* ... */ }, update: async ({ listKey, operation, inputData, item, resolvedData, context, }) => { /* ... */ }, delete: async ({ listKey, operation, item, context, }) => { /* ... */ }, }, }, fields: { someFieldName: text({ hooks: { beforeOperation: { create: async ({ listKey, fieldKey, operation, inputData, inputFieldData, resolvedData, resolvedFieldData, context, }) => { /* ... */ }, update: async ({ listKey, fieldKey, operation, inputData, inputFieldData, item, itemField, resolvedData, resolvedFieldData, context, }) => { /* ... */ }, delete: async ({ listKey, fieldKey, operation, item, itemField, context, }) => { /* ... */ }, }, }, }), }, }), }, }); ``` ### afterOperation The `afterOperation` hook is used to perform side effects after the data has been saved to the database (for a `create` or `update` operation), or deleted from the database (for `delete` operations). | Argument | Description | | :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `listKey` | The key of the list being operated on. | | `fieldKey` | The key of the field being operated on (field hooks only). | | `operation` | The operation being performed (`'create'`, `'update'`, or `'delete'`). | | `inputData` | The value of `data` passed into the mutation. `undefined` for `delete` operations. | | `inputFieldData` | The value of the field from the input data (field hooks only, `undefined` for `delete` operations). | | `originalItem` | The original value of the item being updated or deleted, `undefined` for `create` operations. This object is an internal database item. [DB API](../context/db-items) for more details on internal database items. | | `originalItemField` | The value of the field from the original item (field hooks only, `undefined` for `create` operations). | | `item` | The new value of the item being updated or created, `undefined` for `delete` operations. This object is an internal database item. [DB API](../context/db-items) for more details on internal database items. | | `itemField` | The value of the field from the new item (field hooks only, `undefined` for `delete` operations). | | `resolvedData` | A [`resolved data`](#resolved-data-stages) object. The resolved data value after all data resolver stages have been completed. `undefined` for `delete` operations. | | `resolvedFieldData` | The resolved value for this specific field (field hooks only, `undefined` for `delete` operations). | | `context` | The [`KeystoneContext`](../context/overview) object of the originating GraphQL operation. | ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ hooks: { afterOperation: { create: async ({ listKey, operation, inputData, item, resolvedData, context, }) => { /* ... */ }, update: async ({ listKey, operation, inputData, originalItem, item, resolvedData, context, }) => { /* ... */ }, delete: async ({ listKey, operation, originalItem, context, }) => { /* ... */ }, }, }, fields: { someFieldName: text({ hooks: { afterOperation: { create: async ({ listKey, fieldKey, operation, inputData, inputFieldData, item, itemField, resolvedData, resolvedFieldData, context, }) => { /* ... */ }, update: async ({ listKey, fieldKey, operation, inputData, inputFieldData, originalItem, originalItemField, item, itemField, resolvedData, resolvedFieldData, context, }) => { /* ... */ }, delete: async ({ listKey, fieldKey, operation, originalItem, originalItemField, context, }) => { /* ... */ }, }, }, }), }, }), }, }); ``` ## Resolved data stages Create and update operations take a `data` value for a single item from the GraphQL input and then perform a number of _data resolving_ steps before writing the final value to the database. At each stage of the data resolving process, the value of `resolvedData` can be modified or augmented. The final value of `resolvedData` is the value that will be validated and saved to the database. The data resolving steps are applied in the following order: 1. Initialisation: Set the value of `resolvedData` to the `data` input value from the GraphQL mutation. 2. Defaults (built in, `create` only): Any fields which have a default value and are `undefined` in `resolvedData` will be set to their default value. 3. Relationships (built in): The values for relationship fields on `resolvedData` are [Prisma nested write objects](https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries/#connect-an-existing-record). This is the format expected when saving relationship fields to the database. Any nested create operations are performed during this phase. Their IDs are returned, and combined with any `connect` inputs. All items provided for `connect`, `set`, and `disconnect` are checked to ensure they exist. For to-many relationships, an object with the shape `{ connect: [...], set: [...], disconnect: [...] }` is returned. For to-one relationships, an object with the shape `{ connect }` or `{ disconnect: true }` is returned. 4. Field values (built in): Some fields types take the value given in the GraphQL operation and convert it into a different type or format to be saved to the database. 5. Field hooks (user defined): A `resolveInput` field hook can return a new value for its field, which will the current field value on `resolvedData`. 6. List hooks (user defined): A `resolveInput` list hook can return a new value for the entire `resolvedData` object. ## Related resources {% related-content %} {% well heading="Hooks Guide" href="/docs/guides/hooks" %} Learn how to use Hooks within your schema to extend Keystone’s powerful CRUD GraphQL APIs with your own business logic. {% /well %} {% /related-content %} --- # Session Source: https://keystonejs.com/docs/config/session The `session` property of the [system configuration](./config) object allows you to configure session management of your Keystone system. It has a TypeScript type of `SessionStrategy`. In general you will use `SessionStrategy` objects from the `@keystone-6/core/session` package, rather than writing this yourself. ```typescript import { config } from '@keystone-6/core'; import { statelessSessions } from '@keystone-6/core/session'; export default config({ session: statelessSessions({ secret: 'ABCDEFGH1234567887654321HGFEDCBA', ironOptions: { /* ... */ }, maxAge: 60 * 60 * 24, secure: true, path: '/', domain: 'localhost', sameSite: 'lax', }), /* ... */ }); ``` ## Stateless vs stored sessions Keystone supports both stateless and stored sessions. In a stateless session all session data is stored in a cookie. In a stored session a session ID is stored in the cookie, and this ID is used to save and load data from a data store on the server. All cookies are encrypted with [`@hapi/iron`](https://hapi.dev/module/iron/). Both `statelessSessions()` and `storedSessions()` accept a common set of arguments which control the behaviour of the cookies. `storedSessions()` accepts an additional `store` argument, which is an object capable of loading and storing session data. ```typescript import { config } from '@keystone-6/core'; import { statelessSessions, storedSessions } from '@keystone-6/core/session'; export default config({ // Stateless session: statelessSessions({ /* ... */ }), // Stored session: storedSessions({ store: { /* ... */ }, /* ... */ }), /* ... */ }); ``` Options - `secret` (required): The secret used by `@hapi/iron` for encrypting the cookie data. Must be at least 32 characters long. - `ironOptions`: Additional options to be passed to `Iron.seal()` and `Iron.unseal()` when encrypting and decrypting the cookies. See the [`@hapi/iron` docs](https://hapi.dev/module/iron/api/?v=6.0.0#options) for details. - `maxAge` (default: `60 * 60 * 8` (8 hours)): The number of seconds until the [cookie expires](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie). - `secure` (default: `process.env.NODE_ENV === 'production`): If `true`, the cookie is only sent to the server when a request is made with the `https:` scheme (except on localhost), and therefore is more resistent to man-in-the-middle attacks. **Note:** Do not assume that `secure` prevents all access to sensitive information in cookies (session keys, login details, etc.). Cookies with this attribute can still be read/modified with access to the client's hard disk, or from JavaScript if the HttpOnly cookie attribute is not set. **Note:** Insecure sites (`http:`) can't set cookies with the `secure` attribute (since Chrome 52 and Firefox 52). For Firefox, the `https:` requirements are ignored when the `secure` attribute is set by localhost (since Firefox 75). - `path` (default: `'/'`): A path that must exist in the requested URL, or the browser won't send the cookie header. The forward slash (`/`) character is interpreted as a directory separator, and subdirectories will be matched as well: for `path: '/docs'`, `/docs`, `/docs/Web/`, and `/docs/Web/HTTP` will all match. - `domain` (default: current document URL): Host to which the cookie will be sent. See [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes) for more details on the `domain` cookie attribute. **Note**: Only one domain is allowed. If a domain is specified then subdomains are always included. - `sameSite` (default: `'lax'`): Controls whether the cookie is sent with cross-origin requests. Can be one of `true`, `false`, `'strict'`, `'lax'` or `'none'`. See [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes) for more details on the `sameSite` cookie attribute. **Note**: The `secure` attribute must also be set when `sameSite` is set to `none`! ### Session stores When using `storedSessions` you need to pass in a session store as the `store` option. This `store` option must be either a `SessionStore` object, or a function `({ maxAge }) => { ... }` which returns a `SessionStore` object. A `SessionStore` object has the following interface: ```typescript const store = { set: async (sessionId, data) => { /* ... */ }, get: async (sessionId) => { /* ... */ return data; }, delete: async (sessionId) => { /* ... */ }, }; ``` Interface: - `set`: Set a value `data` for the key `sessionId`. - `get`: Get the `data` value associated with `sessionId`. - `delete`: Delete the entry associated with `sessionId` from the session store. ## Session context If you configure your Keystone session with session management then the [`KeystoneContext`](../context/overview) type will include the following session related properties. - `session`: An object representing the session data. The value will depend on the value passed into `context.sessionStrategy.start()`. - `sessionStrategy`: an object that, when using `statelessSessions` or `storedSessions` from `@keystone-6/core/session` includes the following functions: - `get({context})`: a function that returns a `session` object based on `context` - this needs to be a `context` with a valid `req` (using `context.withRequest`). This function is called by Keystone to get the value of `context.session` - `start({context, data})`: a function that, given a valid `context.res` starts a new session containing what is passed into `data`. - `end({context})`: a function that, given a valid `context.res` ends a session. The `start` and `end` functions will be used by [authentication mutations](./auth) to start and end authenticated sessions. These mutations will set the value of `session` to include the values `{ listKey, itemId }`. ## Related resources {% related-content %} {% well heading="Config API Reference" href="/docs/config/config" %} The API to configure all the parts of your Keystone system. {% /well %} {% well heading="Example Project: Authentication" href="https://github.com/keystonejs/keystone/tree/main/examples/with-auth" target="_blank" %} Adds password-based authentication to the Task Manager starter project. {% /well %} {% /related-content %} --- # Fields Source: https://keystonejs.com/docs/fields/overview {% hint kind="warn" %} We recently improved this API. If you were using it prior to October 5th 2021, [read this guide](/releases/2021-10-05) for details on how to upgrade. {% /hint %} The `fields` option of a [list configuration](../config/lists) defines the names, types, and configuration of the fields in the list. This configuration option takes an object with field names as keys, and configured field types as values. This document covers the different field types which are available and the configuration options they accept. To see how to access fields in the GraphQL API please see the [GraphQL API](../graphql/overview) docs. ```typescript import { config, list } from '@keystone-6/core' import { // Scalar types bytes, checkbox, integer, bigInt, json, float, decimal, password, select, multiselect, text, timestamp, calendarDay, // Relationship type relationship, // Virtual type virtual, // File types file, image, } from '@keystone-6/core/fields' // Complex types import { document } from '@keystone-6/fields-document' import { cloudinaryImage } from '@keystone-6/cloudinary' export default config({ lists: { SomeListName: list({ fields: { someFieldName: text({ /* ... */ }), /* ... */ }, }), /* ... */ }, /* ... */ }) ``` ## Common configuration All field types accept a common set of configuration options. All options are optional. Options: - `isFilterable` (default: `true`): If `false`, the GraphQL API and admin UI will not support filtering by this field. If `true` (default), the GraphQL API and Admin UI will support filtering by this field. If a function is provided, it will be evaluated dynamically each time this field is used as a filter in a GraphQL operation. If the function returns `false`, the operation will return an error indicating that filtering on this field is not allowed. - `isOrderable` (default: `true`): If `false`, the GraphQL API and admin UI will not support ordering by this field. If `true` (default), the GraphQL API and Admin UI will support ordering by this field. If a function is provided, it will be evaluated dynamically each time this field is used as an ordering field in a GraphQL operation. If the function returns `false`, the operation will return an error indicating this ordering by this field is not allowed. - `access`: Defines the [Access Control](../guides/auth-and-access-control) rules for the field. See the [Access Control API](../config/access-control) for full details on the available access control options. - `hooks`: The `hooks` option defines [hook](../guides/hooks) functions for the field. Hooks allow you to execute code at different stages of the mutation lifecycle. See the [Hooks API](../config/hooks) for full details on the available hook options. - `ui`: Controls how the field is displayed in the Admin UI. - `label`: The label displayed for this field in the Admin UI. Defaults to a human readable version of the field name. - `views`: A module path that is resolved from where `keystone start` is run, resolving to a module containing code to replace or extend the Admin UI components for this field. See the [Custom Field Views](../guides/custom-field-views) guide for details on how to use this option. - `createView.fieldMode` (default: `'edit'`): Controls the create view page of the Admin UI. Can be one of `['edit', 'hidden']`, or an async function with an argument `{ session, context }` that returns one of `['edit', 'hidden']`. Defaults to the list's `fieldDefaults.ui.createView.fieldMode` config if defined. See the [Lists API](../config/lists#ui) for details. - `createView.isRequired` (default: `undefined`): Controls whether the field is marked as required in the create view. Can be a boolean or an async function with an argument `{ session, context }` that returns a boolean. When `true`, the Admin UI will show the field as required. - `itemView.fieldMode` (default: `'edit'`): Controls the item view page of the Admin UI. Can be one of `'edit' | 'read' | 'hidden'`, a conditional filter object, or an async function with an argument `{ session, context, listKey, fieldKey, item, itemField }` that returns a field mode or conditional filter object. The `item` argument may be `null`. A function that depends on `item` is initially called with `item: null`. Do not return `'hidden'` in this case. Keystone treats `'hidden'` as an indication that it does not need to fetch the field for the item page, so the function cannot later show the field after it receives the item. Return `'read'` until the item is available instead: ```typescript // Broken: the field will not be fetched for the item page fieldMode: ({ item }) => { if (item === null) return 'hidden' return item.status === 'archived' ? 'hidden' : 'edit' } // Good: keep fetching the field until the item is available fieldMode: ({ item }) => { if (item === null) return 'read' return item.status === 'archived' ? 'hidden' : 'edit' } ``` When a condition can be expressed as a filter, you should prefer that instead of a function. This allows the UI to respond immediately without saving the item. For example, this hides the field when `status` is `'archived'`: ```typescript fieldMode: { hidden: { status: { equals: 'archived' } }, } ``` If the mode depends on both the session and item fields, prefer a function for the session-based decision and return a conditional filter object for the item-based decision. For example, this hides the field from non-admin sessions and uses `status` to determine the mode for admins: ```typescript fieldMode: ({ context }) => { if (context.session?.data.role !== 'admin') return 'hidden' return { hidden: { status: { equals: 'archived' } }, } } ``` `fieldMode` only controls the Admin UI. Use [field access control](../config/access-control#field-access-control) to protect field values. Defaults to the list's `fieldDefaults.ui.itemView.fieldMode` config if defined. See the [Lists API](../config/lists#ui) for details. - `itemView.fieldPosition` (default: `form`): Controls which side of the page the field is placed in the Admin UI. Can be either `form` or `sidebar`, or an async function with an argument `{ session, context, listKey, fieldKey, item, itemField }` that returns one of `['form', 'sidebar']`. The `item` argument may be `null`. `form` or blank places the field on the left hand side of the item view. `sidebar` places the field on the right hand side under the ID field - `itemView.isRequired` (default: `undefined`): Controls whether the field is marked as required in the item view. Can be a boolean or an async function with an argument `{ session, context }` that returns a boolean. When `true`, the Admin UI will show the field as required. - `listView.fieldMode` (default: `'read'`): Controls the list view page of the Admin UI. Can be one of `['read', 'hidden']`, or an async function with an argument `{ session, context }` that returns one of `['read', 'hidden']`. Defaults to the list's `fieldDefaults.ui.listView.fieldMode` config if defined. See the [Lists API](../config/lists#ui) for details. - `graphql`: Configures certain aspects of the GraphQL API. - `cacheHint` (default: `undefined`): Allows you to specify the [dynamic cache control hints](https://www.apollographql.com/docs/apollo-server/performance/caching/#in-your-resolvers-dynamic) used for queries to this list. - `isNonNull` (default: `undefined`): Controls whether this field uses non-nullable GraphQL types. This option accepts either `true` to make the field non-nullable for all operations, or an object with the below fields. - `isNonNull.read` (default: `false`): Changing this to `true` will change the GraphQL output type to be non-nullable. If you have 'read' field access control, this will be rejected with a runtime `Error` as `null` may be returned if the access control returns false. - `isNonNull.create` (default: `false`): Enforce that this field is non-nullable in the \*CreateInput for the respective list item. Fields that have a `defaultValue` only have that default value included in the GraphQL if this field is `true`. - `isNonNull.update` (default: `false`): Enforce that this field is non-nullable in the \*UpdateInput for the respective list item. This field additionally results in the AdminUI always sending this particular field in updates, including when it has not changed. - `omit` (default: 'undefined'): Controls whether this field appears in the autogenerated types of the GraphQL API This option accepts either `true`, or an object with the below fields. If you specify `true` then the field will be excluded from all input and output types in the GraphQL API. - `omit.read` (default: `false`): A boolean omits item output, filtering, and ordering together. An object with optional `item`, `filter`, and `order` booleans controls them independently. - `omit.create` (default: `false`): If you specify `true`, then the field will be excluded from the list's CreateInput GraphQL type. - `omit.update` (default: `false`): If you specify `true`, then the field will be excluded from the list's UpdateInput GraphQL type. For Admin UI conditional config such as `fieldMode` and `isRequired`, filter objects can combine field predicates with nested `AND`, `OR`, and `NOT` groups. These are evaluated client-side in the Admin UI and do not change the GraphQL `where` input API. ```typescript export default config({ lists: { SomeListName: list({ fields: { someFieldName: text({ isFilterable: ({ context, session, fieldKey, listKey }) => true, isOrderable: ({ context, session, fieldKey, listKey }) => true, access: { /* ... */ }, hooks: { /* ... */ }, ui: { label: '...', views: './path/to/viewsModule', createView: { fieldMode: ({ session, context }) => 'edit', }, itemView: { fieldMode: { read: { status: { equals: 'archived' }, NOT: { canEdit: { equals: true } }, }, }, }, listView: { fieldMode: ({ session, context }) => 'read', }, }, graphql: { cacheHint: { maxAge: 60, scope: CacheScope.Private }, omit: { read: true, create: true, update: true, }, }, }), /* ... */ }, }), /* ... */ }, /* ... */ }) ``` ## Groups Fields can be grouped together in the Admin UI using the `group` function, with a customisable `label` and `description`. ```typescript import { config, list, group } from '@keystone-6/core' import { text } from '@keystone-6/core/fields' export default config({ lists: { SomeListName: list({ fields: { ...group({ label: 'Group label', description: 'Group description', fields: { someFieldName: text({ /* ... */ }), /* ... */ }, }), /* ... */ }, }), /* ... */ }, /* ... */ }) ``` Groups also support UI `fieldDefaults` for `createView`, `itemView`, and `listView`, which apply to all fields within the group unless overridden at the field level: ```typescript ...group({ label: 'Advanced', description: 'Advanced settings', fieldDefaults: { ui: { createView: { fieldMode: 'hidden' }, itemView: { fieldMode: 'read' }, listView: { fieldMode: 'hidden' }, }, }, fields: { someFieldName: text({ /* ... */ }), }, }) ``` ## Scalar types - [BigInt](./bigint) - [Calendar Day](./calendarday) - [Checkbox](./checkbox) - [Decimal](./decimal) - [Float](./float) - [Integer](./integer) - [JSON](./json) - [Multiselect](./multiselect) - [Password](./password) - [Select](./select) - [Text](./text) - [Timestamp](./timestamp) ## Relationship type - [Relationship](./relationship) ## Virtual type - [Virtual](./virtual) ## File types - [File](./file) - [Image](./image) ## Complex types - [Document](./document) - [Cloudinary Image](./cloudinaryimage) ## Related resources {% related-content %} {% well heading="Lists API Reference" href="/docs/config/lists" %} The API to configure your options used with the `list()` function. {% /well %} {% well heading="GraphQL API Reference" href="/docs/graphql/overview" %} A complete CRUD (create, read, update, delete) GraphQL API derived from the list and field names you configure in your system. {% /well %} {% /related-content %} --- # BigInt Source: https://keystonejs.com/docs/fields/bigint A `bigInt` field represents a 64 bit signed integer value. When interacting with the field with GraphQL, values will be strings. When interacting with the field in hooks, values will be [Javascript `BigInt`s](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#bigint_type). Javascript `BigInt`s are used since [integers in Javascript are unsafe beyond 2{% sup %}53{% /sup %}](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER). Options: - `defaultValue` (default: `undefined`): Can be either a bigint value, `null`, or `{ kind: 'autoincrement' }`. This value will be used for the field when creating items if no explicit value is set. `{ kind: 'autoincrement' }` is only supported on PostgreSQL. - `db.isNullable` (default: `validation.isRequired ? false : true`): If `false` then this field will be made non-nullable in the database and it will never be possible to set as `null`. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null`. It validate this when creating and updating an item through the GraphQL API or the Admin UI. It will also default `db.isNullable` to false. - `validation.min`(default: `(-2n) ** 63n`): This describes the minimum number allowed. If you attempt to submit a number under this, you will get a validation error. - `validation.max`(default: `2n ** 63n - 1n`): This describes the maximum number allowed. If you attempt to submit a number over this, you will get a validation error. - If you want to specify a range within which the numbers must fall, specify both a minimum and a maximum value. - `isIndexed` (default: `false`) - If `true` then this field will be indexed by the database. - If `'unique'` then all values of this field must be unique. - `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future, you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. - `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. This is only allowed when you have no create access control because otherwise, the item will always fail access control if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. ```typescript import { config, list } from '@keystone-6/core'; import { bigInt } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: bigInt({ defaultValue: 0n, db: { map: 'my_bigint' }, validation: { isRequired: true, }, isIndexed: 'unique', }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Bytes Source: https://keystonejs.com/docs/fields/bytes A `bytes` field represents binary data as a `Uint8Array` in JavaScript. Options: - `defaultValue` (default: `null`): This `Uint8Array` value will be used for the field when creating items if no explicit value is set. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database. - `db.isNullable` (default: `true`): If `false`, this field will be non-nullable in the database. - `db.nativeType`: Changes the [Prisma native database type attribute](https://www.prisma.io/docs/orm/reference/prisma-schema-reference#bytes) from the default Prisma type for your database. The native type is not customisable on SQLite. - `validation.isRequired` (default: `false`): If `true`, this field can never be set to `null`. - `validation.length.min` (default: `undefined`): The minimum length in bytes. - `validation.length.max` (default: `undefined`): The maximum length in bytes. - `isIndexed` (default: `false`) - If `true` then this field will be indexed by the database. - If `'unique'` then all values of this field must be unique. - `graphql.scalar` (default: Keystone's `Bytes` scalar): Allows you to provide a custom GraphQL scalar for serialising and parsing values. ```typescript import { config, list } from '@keystone-6/core'; import { bytes } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: bytes({ validation: { length: { max: 1024 } }, db: { map: 'my_bytes' }, }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Calendar Day Source: https://keystonejs.com/docs/fields/calendarday A `calendarDay` field represents a date value only in ISO8601 format. On PostgreSQL and MySQL this is represented with the native date type. On SQLite this is represented as a string. Options: - `defaultValue` (default: `undefined`): Can be a string value with a date string in ISO8601 format. This value will be used for the field when creating items if no explicit value is set. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `db.isNullable` (default: `validation.isRequired ? false : true`): If `false` then this field will be made non-nullable in the database and it will never be possible to set as `null`. - `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null`. It validate this when creating and updating an item through the GraphQL API or the Admin UI. It will also default `db.isNullable` to false. - `isIndexed` (default: `false`) - If `true` then this field will be indexed by the database. - If `'unique'` then all values of this field must be unique. - `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future, you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. - `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. This is only allowed when you have no create access control because otherwise, the item will always fail access control if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. ```typescript import { config, list } from '@keystone-6/core'; import { calendarDay } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: calendarDay({ defaultValue: '1970-01-01', db: { map: 'my_date' }, validation: { isRequired: true }, isIndexed: 'unique', }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Checkbox Source: https://keystonejs.com/docs/fields/checkbox A `checkbox` field represents a boolean (`true`/`false`) value. Options: - `defaultValue` (default: `false`): This value will be used for the field when creating items if no explicit value is set. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `graphql.isNonNull.read` (default: `false`): If you have no read access control and you don't intend to add any in the future, you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. - `graphql.isNonNull.create` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. This is only allowed when you have no create access control because otherwise, the item will always fail access control if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. ```typescript import { config, list } from '@keystone-6/core'; import { checkbox } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: checkbox({ defaultValue: true, db: { map: 'my_checkbox' }, graphql: { isNonNull: { read: true, create: true, }, } }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Cloudinary Image Source: https://keystonejs.com/docs/fields/cloudinaryimage Upload images to your Cloudinary account via Keystone's Admin UI. - `cloudinary`: Configuration for the connected Cloudinary account. - `cloudName` - TODO - `apiKey` - TODO - `apiSecret` - TODO - `folder` - TODO ```typescript import { config, list } from '@keystone-6/core'; import { cloudinaryImage } from '@keystone-6/cloudinary'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: cloudinaryImage({ cloudinary: { cloudName: process.env.CLOUDINARY_CLOUD_NAME, apiKey: process.env.CLOUDINARY_API_KEY, apiSecret: process.env.CLOUDINARY_API_SECRET, folder: process.env.CLOUDINARY_API_FOLDER, }, }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Decimal Source: https://keystonejs.com/docs/fields/decimal A `decimal` field represents a decimal value. Options: - `defaultValue` (default: `undefined`): Can be a decimal value written as a string, or `null`. This value will be used for the field when creating items if no explicit value is set. - `precision` (default: `18`): Maximum number of digits that are present in the number. - `scale` (default: `4`): Maximum number of decimal places. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `db.isNullable` (default: `validation.isRequired ? false : true`): If `false` then this field will be made non-nullable in the database and it will never be possible to set as `null`. - `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null`. It validate this when creating and updating an item through the GraphQL API or the Admin UI. It will also default `db.isNullable` to false. - `validation.min` (default: `undefined`): This describes the minimum number allowed. If you attempt to submit a number under this, you will get a validation error. - `validation.max` (default: `undefined`): This describes the maximum number allowed. If you attempt to submit a number over this, you will get a validation error. - If you want to specify a range within which the numbers must fall, specify both a minimum and a maximum value. - `isIndexed` (default: `false`) - If `true` then this field will be indexed by the database. - If `'unique'` then all values of this field must be unique. - `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future, you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. - `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. This is only allowed when you have no create access control because otherwise, the item will always fail access control if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. ```typescript import { config, list } from '@keystone-6/core'; import { decimal } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: decimal({ defaultValue: '3.142', precision: 12, scale: 3, db: { map: 'my_decimal' }, validation: { isRequired: true, max: '10000', min: '2', }, isIndexed: 'unique', }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Document Source: https://keystonejs.com/docs/fields/document A highly customizable, [Slate](https://docs.slatejs.org/)-based, rich text editor that lets content creators quickly and easily edit content in your system. See the [Document Field guide](https://keystonejs.com/docs/guides/document-fields), [demo](https://keystonejs.com/docs/guides/document-field-demo) and [example project](https://github.com/keystonejs/keystone/tree/main/examples/document-field) for details. Options: - `relationships` - `componentBlocks` - `formatting` - `links` - `dividers` - `layouts` ```typescript import { config, list } from '@keystone-6/core'; import { document } from '@keystone-6/fields-document'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: document({ relationships: { /* ... */ }, componentBlocks: { block: { /* ... */ }, /* ... */ }, formatting: { /* ... */ }, links: true, dividers: true, layouts: [/* ... */], }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # File Source: https://keystonejs.com/docs/fields/file A `file` field represents a file of any type. See [the Images and Files guide](../guides/images-and-files) for details on how to configure `storage` for the `file` field type. ```typescript import { config, list } from '@keystone-6/core'; import { file } from '@keystone-6/core/fields'; import fs from 'node:fs/promises'; export default config({ lists: { SomeListName: list({ fields: { repo: file({ storage: { async put(key, stream) { await fs.writeFile(`public/files/${key}`, stream) }, async delete(key) { await fs.unlink(`public/files/${key}`) }, url(key) { return `http://localhost:3000/files/${key}` }, }, }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` Options: - `storage`(required): An object that defines how to upload (`put`), delete (`delete`) and get a URL to the file (`url`). --- # Float Source: https://keystonejs.com/docs/fields/float A `float` field represents a floating point value. Options: - `defaultValue` (default: `undefined`): Can be a finite float value, or `null`. This value will be used for the field when creating items if no explicit value is set. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `db.isNullable` (default: `validation.isRequired ? false : true`): If `false` then this field will be made non-nullable in the database and it will never be possible to set as `null`. - `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null`. It validate this when creating and updating an item through the GraphQL API or the Admin UI. It will also default `db.isNullable` to false. - `validation.min` (default: `undefined`): This describes the minimum number allowed. If you attempt to submit a number under this, you will get a validation error. - `validation.max` (default: `undefined`): This describes the maximum number allowed. If you attempt to submit a number over this, you will get a validation error. - `isIndexed` (default: `false`) - If `true` then this field will be indexed by the database. - If `'unique'` then all values of this field must be unique. - `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future, you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. - `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. This is only allowed when you have no create access control because otherwise, the item will always fail access control if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. ```typescript import { config, list } from '@keystone-6/core'; import { float } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: float({ defaultValue: 3.14159, db: { map: 'my_float' }, validation: { isRequired: true, }, isIndexed: 'unique', }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Image Source: https://keystonejs.com/docs/fields/image An `image` field represents an image file, i.e. `.jpg`, `.png`, `.webp`, or `.gif`. See [the Images and Files guide](../guides/images-and-files) for details on how to configure `storage` for the `image` field type. ```typescript import { config, list } from '@keystone-6/core'; import { image } from '@keystone-6/core/fields'; import fs from 'node:fs/promises'; export default config({ lists: { SomeListName: list({ fields: { avatar: image({ storage: { async put(key, stream) { await fs.writeFile(`public/files/${key}`, stream) }, async delete(key) { await fs.unlink(`public/files/${key}`) }, url(key) { return `http://localhost:3000/files/${key}` }, }, }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` Options: - `storage`(required): An object that defines how to upload (`put`), delete (`delete`) and get a URL to the file (`url`). --- # Integer Source: https://keystonejs.com/docs/fields/integer An `integer` field represents a 32 bit signed integer value. Options: - `defaultValue` (default: `undefined`): Can be either an integer value, `null`, or `{ kind: 'autoincrement' }`. This value will be used for the field when creating items if no explicit value is set. `{ kind: 'autoincrement' }` is only supported on PostgreSQL. - `db.isNullable` (default: `validation.isRequired ? false : true`): If `false` then this field will be made non-nullable in the database and it will never be possible to set as `null`. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null`. It validate this when creating and updating an item through the GraphQL API or the Admin UI. It will also default `db.isNullable` to false. - `validation.min` (default: `-2147483648`): This describes the minimum number allowed. If you attempt to submit a number under this, you will get a validation error. - `validation.max` (default: `2147483647`): This describes the maximum number allowed. If you attempt to submit a number over this, you will get a validation error. - If you want to specify a range within which the numbers must fall, specify both a minimum and a maximum value. - `isIndexed` (default: `false`) - If `true` then this field will be indexed by the database. - If `'unique'` then all values of this field must be unique. - `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future, you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. - `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. This is only allowed when you have no create access control because otherwise, the item will always fail access control if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. ```typescript import { config, list } from '@keystone-6/core'; import { integer } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: integer({ defaultValue: 0, db: { map: 'my_integer' }, validation: { isRequired: true, }, isIndexed: 'unique', }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # JSON Source: https://keystonejs.com/docs/fields/json A `json` field represents a JSON blob. Currently the `json` field is non-orderable and non-filterable. - `defaultValue` (default: `null`): Can be set to any JSON value. This value will be used for the field when creating items if no explicit value is set. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database ```typescript import { config, list } from '@keystone-6/core'; import { json } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: json({ defaultValue: { something: true }, db: { map: 'my_json' }, }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Multiselect Source: https://keystonejs.com/docs/fields/multiselect A `multiselect` field represents the selection of a set of values from the defined `options`. Values can be either strings, integers, or enum values, as determined by the `type` option. This will determine their GraphQL data type. Unlike the `select` field, the `type` will not change the database type, `multiselect` fields are always stored as a json array in the database. Options: - `type` (default: `'string'`): Sets the type of the values of this field. Must be one of `['string', 'enum', 'integer']`. - `options`: An array of `{ label, value }`. `label` is a string to be displayed in the Admin UI. `value` is either a `string` (for `{ type: 'string' }` or `{ type: 'enum' }`), or a `number` (for `{ type: 'integer' }`). The `value` will be used in the GraphQL API and stored in the database. - `defaultValue` (default: `[]`): This value will be used for the field when creating items if no explicit value is set. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future, you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. - `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. This is only allowed when you have no create access control because otherwise, the item will always fail access control if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. ```typescript import { config, list } from '@keystone-6/core'; import { multiselect } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: multiselect({ type: 'enum', options: [ { label: '...', value: '...' }, /* ... */ ], defaultValue: ['...'], db: { map: 'my_multiselect' }, }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Password Source: https://keystonejs.com/docs/fields/password A `password` field represents an encrypted password value. Options: - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `db.isNullable` (default: `validation.isRequired ? false : true`): If `false` then this field will be made non-nullable in the database and it will never be possible to set as `null`. - `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null`. It validate this when creating and updating an item through the GraphQL API or the Admin UI. It will also default `db.isNullable` to false. - `validation.length.min` (default: `8`): This describes the minimum length allowed. If you attempt to submit a string shorter than this, you will get a validation error. - `validation.length.max` (default: `undefined`): This describes the maximum length allowed. If you attempt to submit a string longer than this, you will get a validation error. - `validation.match` (default: `undefined`): This describes a pattern that values for this field must match - `validation.match.regex`: The regular expression - `validation.match.explanation` (default: `${fieldLabel} must match ${validation.match.regex}`): A message shown in the Admin when a value doesn't match the regex and returned as a validation error from the GraphQL API - `validation.rejectCommon` (default: `false`): Rejects passwords from a list of commonly used passwords. - `kdf` (default: `{ hash: (secret) => bcryptjs.hash(secret, 10), compare: (secret, hash) => bcryptjs.compare(secret, hash) }`): An object with `hash` and `compare` functions for hashing and comparing passwords. The default uses `bcryptjs` and rejects truncated inputs. ```typescript import { config, list } from '@keystone-6/core'; import { password } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: password({ db: { map: 'password_field' }, validation: { length: { min: 10, max: 1000 }, isRequired: true, rejectCommon: true, }, }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Relationship Source: https://keystonejs.com/docs/fields/relationship A `relationship` field represents a relationship between two lists. Read our [relationships guide](../guides/relationships) for details on Keystone’s relationship model and how to configure them in your project. - `ref` (required): A string of the form `` or `.`. - `many` (default: `false`): Configures the cardinality of the relationship. - `db.foreignKey`: When `true` or an object, ensures the foreign Key for two-sided relationships is stored in the table for this list (only available on single relationships, and not on both sides of a 1-to-1 relationship) - `map`: Changes the column name in the database - `ui`: Configures the display mode of the field in the Admin UI - `displayMode` (default: `select`): Controls how the relationship is displayed in the Admin UI. Options: `'select'`, `'cards'`, `'count'`, or `'table'`. - `'count'` and `'table'` only support `many: true` relationships (`'count'` will require `itemView: { fieldMode: 'read' }` to be set) - `'table'` displays related items in a tabular format. - `labelField`: The field path from the related list to use for item labels in the select. Defaults to the `labelField` configured on the related list. - `searchFields`: The fields used by the Admin UI when searching by this relationship on the list view and in relationship fields. Nominated fields need to support the `contains` filter. Defaults to `ui.listView.searchField` on the related list. - `filter`: A where input filter to apply when showing items in the select. Defaults to the `ui.listView.initialFilter` (if set) on the related list. - `sort`: The sort to apply when showing items in the select, e.g. `{ field: 'name', direction: 'ASC' }`. Defaults to the `ui.listView.initialSort` (if set) on the related list. - `hideCreate` (default: `false`). If `true`, the "Create related item" button is not shown in the item view. ```typescript import { config, list } from '@keystone-6/core'; import { relationship } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: relationship({ ref: '...', many: false, db: { foreignKey: { map: 'foreign_id', }, }, ui: { hideCreate: false, displayMode: 'select', labelField: 'name', displayMode: 'cards', cardFields: [...], linkToItem: true, removeMode: 'disconnect', inlineCreate: { fields: [...] }, inlineEdit: { fields: [...] }, inlineConnect: true, // Display mode: 'count' // requires many: true above displayMode: 'count', }, }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` ## GraphQL For to-many relationships (`many: true`), the update input type includes `connect`, `disconnect`, and `set` fields: - `connect`: Nominate an item to relate to this item. - `disconnect`: Disconnect any relationships to this item. - `set`: Replace the set of related items (disconnects, then connects the nominated items). --- # Select Source: https://keystonejs.com/docs/fields/select A `select` field represents the selection of one of fixed set of values. Values can be either strings, integers, or enum values, as determined by the `type` option. This will determine their GraphQL data type, as well as their database storage type except for `enum` on SQLite where the GraphQL type will be an enum but it will be represented as a string in the database. Options: - `type` (default: `'string'`): Sets the type of the values of this field. Must be one of `['string', 'enum', 'integer']`. - `options`: An array of `{ label, value }`. `label` is a string to be displayed in the Admin UI. `value` is either a `string` (for `{ type: 'string' }` or `{ type: 'enum' }`), or a `number` (for `{ type: 'integer' }`). The `value` will be used in the GraphQL API and stored in the database. - `defaultValue` (default: `undefined`): This value will be used for the field when creating items if no explicit value is set. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `db.isNullable` (default: `validation.isRequired ? false : true`): If `false` then this field will be made non-nullable in the database and it will never be possible to set as `null`. - `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null`. It validate this when creating and updating an item through the GraphQL API or the Admin UI. It will also default `db.isNullable` to false. - `isIndexed` (default: `false`) - If `true` then this field will be indexed by the database. - If `'unique'` then all values of this field must be unique. - `ui.displayMode` (default: `'select'`): Configures the display mode of the field in the Admin UI. Can be one of `['select', 'segmented-control', 'radio']`. - `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future, you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. - `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. This is only allowed when you have no create access control because otherwise, the item will always fail access control if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. ```typescript import { config, list } from '@keystone-6/core'; import { select } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: select({ type: 'enum', options: [ { label: '...', value: '...' }, /* ... */ ], defaultValue: '...', db: { map: 'my_select' }, validation: { isRequired: true, }, isIndexed: 'unique', ui: { displayMode: 'select' }, }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Text Source: https://keystonejs.com/docs/fields/text A `text` field represents a string value. Options: - `defaultValue` (default: `db.isNullable === true ? undefined : ''`): This value will be used for the field when creating items if no explicit value is set. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `db.isNullable` (default: `false`\*): If `true` then this field will be made nullable in the database and it will be possible to set as `null`. - `db.nativeType`: Changes the [Prisma Native database type attibute](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string) from the default Prisma type for your database. For more information see the [Choosing a Database Guide](../guides/choosing-a-database). - `validation.isRequired` (default: `false`\*\*): If `true` then this field can never be set to `null` or an empty string. Unlike `db.isNullable`, this will require that a value with at least 1 character is provided in the Admin UI. It will also validate this when creating and updating an item through the GraphQL API but it will not enforce it at the database level. - `validation.length.min` (default: `0`): This describes the minimum number allowed. If you attempt to submit a string shorter than this, you will get a validation error. - `validation.length.max` (default: `undefined`): This describes the maximum length allowed. If you attempt to submit a string longer than this, you will get a validation error. - `validation.match` (default: `undefined`): This describes a pattern that values for this field must match - `validation.match.regex`: The regular expression - `validation.match.explanation` (default: `${fieldLabel} must match ${validation.match.regex}`): A message shown in the Admin when a value doesn't match the regex and returned as a validation error from the GraphQL API - `isIndexed` (default: `false`) - If `true` then this field will be indexed by the database. - If `'unique'` then all values of this field must be unique. - `ui` (default: `{ displayMode: 'input' }`): Configures the display mode of the field in the Admin UI. Can be one of `['input', 'textarea']`. - `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future, you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. - `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. This is only allowed when you have no create access control because otherwise, the item will always fail access control if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. {% hint kind="tip" %} **\*Warning** Unlike with other `keystone` fields, `db.isNullable` is defaulted to `false` for the text field. This is primarily in the interest of not having to make assumptions about how a `null` value can be represented in a text field in the Admin-UI in the default case. These differences, and the rationale for them, are examined in detail [on GitHub](https://github.com/keystonejs/keystone/discussions/7158). You can opt into this behaviour by explicitly setting `isNullable: true`. {% /hint %} ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: text({ defaultValue: '...', db: { map: 'my_text', nativeType: 'VarChar(40)' }, validation: { isRequired: true }, isIndexed: 'unique', ui: { displayMode: 'textarea' }, }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Timestamp Source: https://keystonejs.com/docs/fields/timestamp A `timestamp` field represents a date time value in ISO8601 format. Options: - `defaultValue` (default: `undefined`): Can be either a string value with a date time string in ISO8601 format or `{ kind: 'now' }`. This value will be used for the field when creating items if no explicit value is set. - `db.map`: Adds a [Prisma `@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map) attribute to this field which changes the column name in the database - `db.isNullable` (default: `validation.isRequired ? false : true`): If `false` then this field will be made non-nullable in the database and it will never be possible to set as `null`. - `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null`. It validate this when creating and updating an item through the GraphQL API or the Admin UI. It will also default `db.isNullable` to false. - `db.updatedAt` (default: `false`) If `true` then this field will add the `@updatedAt` attribute to this field in the Prisma schema. This will update this field to the current time whenever an item is created/updated with the GraphQL API or any other usage of the Prisma Client if this field is not explicitly set in the request. Note this happens at the Prisma Client level, not at the database so if you update an item in your database directly, fields with `db.updatedAt: true` will not automatically update. - `isIndexed` (default: `false`) - If `true` then this field will be indexed by the database. - If `'unique'` then all values of this field must be unique. - `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future, you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. - `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. This is only allowed when you have no create access control because otherwise, the item will always fail access control if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. ```typescript import { config, list } from '@keystone-6/core'; import { timestamp } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: timestamp({ defaultValue: '1970-01-01T00:00:00.000Z', db: { map: 'my_timestamp' }, validation: { isRequired: true }, isIndexed: 'unique', }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Virtual Source: https://keystonejs.com/docs/fields/virtual A `virtual` field represents a value which is computed at read time, rather than stored in the database. See the [virtual fields guide](../guides/virtual-fields) for details on how to use virtual fields. Options: - `field` (required): The GraphQL field that defines the type, resolver and arguments. - `ui.query` (default: `''` ): Defines what the Admin UI should fetch from this field, it's interpolated into a query like this: ```graphql query { item(where: { id: "..." }) { field${ui.query} } } ``` This is only needed when you your field returns a GraphQL type other than a scalar(String and etc.) or an enum or you need to provide arguments to the field. ```typescript import { config, createSchema, g, list } from '@keystone-6/core'; import { virtual } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: virtual({ field: g.field({ type: g.String, args: { something: g.arg({ type: g.Int }) }, resolve(item, args, context, info) { } }) }), /* ... */ }, }), /* ... */ }, /* ... */ }); ``` --- # Context Overview Source: https://keystonejs.com/docs/context/overview The `Context` object is the primary API entry point for all of the run-time functionality of your Keystone system. In GraphQL, a [`context`](https://graphql.org/learn/execution/#root-fields-resolvers) is a value which is provided to every resolver and holds important contextual information such as the currently logged in user, or access to a database. Keystone makes a `Context` object available as the context of all resolvers in the system. The APIs provided by the `Context` object can be used to write the business logic for things like [access control](../guides/auth-and-access-control), [hooks](../guides/hooks), [testing](../guides/testing) and GraphQL [schema extensions](../guides/schema-extension). The `Context` object has the following properties, which are documented below. ```typescript import type { Context } from './generated/keystone/types' const context = { // Query API query, // Database API db, // HTTP request object req, res, // Session API session, sessionStrategy, // GraphQL helpers graphql: { schema, run, raw, }, // New context creators sudo, internal, withRequest, withSession, // Transactions transaction, // Raw Prisma access prisma, } ``` ### HTTP request object `req`: The [IncomingMessage](https://nodejs.org/api/http.html#class-httpincomingmessage) object from the HTTP request. `res`: The [ServerResponse](https://nodejs.org/api/http.html#class-httpserverresponse) object for the HTTP request. ### Query API `query`: The [Query API](./query), which can be used to perform CRUD operations against your GraphQL API and return a queried value. ### Database API `db`: The [Database API](./db-items), which can be used to perform CRUD operations against your GraphQL API and return database objects suitable for return from mutations in [schema extensions](../guides/schema-extension). ### GraphQL helpers `graphql.schema`: The [GraphQL Schema](https://graphql.org/graphql-js/type/#graphqlschema) object, which can be used directly, or via `graphql.raw` and `graphql.run`. `graphql.raw`: An async function which takes `({ query, variables })` and executes the query against the GraphQL schema, returning `{ data, errors }`. `query` can be either a string or a GraphQL [`Document`](https://graphql.org/graphql-js/language/#parse). `variables` is an optional object containing the input variables for the query. The returned object is an [`ExecutionResult`](https://graphql.org/graphql-js/execution/). This query uses the current `context` object as its `context`. `graphql.run`: An async function which takes `({ query, variables })` and executes the query against the GraphQL schema, returning `data`. `query` can be either a string or a GraphQL [`Document`](https://graphql.org/graphql-js/language/#parse). `variables` is an optional object containing the input variables for the query. The returned object is an [`ExecutionResult.data`](https://graphql.org/graphql-js/execution/). Any errors generated by the query will be thrown as an exception. This query uses the current `context` object as its `context`. See the [schema extension guide](../guides/schema-extension) for examples of using these functions. ### Session API If you configure your Keystone system with session management then you will have access to the following properties. See the [session API](../config/session#session-context) for more details. `session`: The current session data object. `sessionStrategy`: an object containing functions(`get`, `start` and `end`) that manipulate a session. See the [session API](../config/session#session-context) for more details. ### New context creators When using the `context.query`, `context.graphql.run`, and `context.graphql.raw` APIs, access control and session information is passed through to these calls from the `context` object. The following functions will create a new `Context` object with this behaviour modified. `sudo()`: A function which returns a new `Context` object that bypasses your `access` control configuration when using `context.query`, `context.db` or `context.graphql`. `internal()`: A function which returns a new `Context` object that bypasses `graphql.omit` on lists and fields, allowing you to query and mutate data that is otherwise omitted from the GraphQL API. `withRequest(req, res)`: A function which returns a new `Context` object with the `.session` determined by your `sessionStrategy.get` function. `withSession(newSession)`: A function which returns a new `Context` object with the `.session` object replaced by the `newSession` argument. ### Database access The `Context` object exposes the underlying database driver directly via `context.prisma`, which is a [Prisma Client](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference) object. {% hint kind="tip" %} **Warning:** Unlike other data access functions (`context.db`, `context.query` or `context.graphql`), `context.prisma` always bypasses the GraphQL schema and your access control. It is the equivalent of using `.sudo()` always. {% /hint %} ### Transactions `transaction(fn, options)`: Runs `fn` inside a Prisma transaction and passes it a new `Context` object backed by the transaction client. The optional `options` argument is forwarded to Prisma's `$transaction` call. ## Related resources {% related-content %} {% well heading="Query API Reference" href="/docs/context/query" %} A programmatic API for running CRUD operations against your GraphQL API. For each list in your system you get an API at `context.query.` {% /well %} {% well heading="DB API Reference" href="/docs/context/db-items" %} The API for running CRUD operations against the internal GraphQL resolvers in your system. It returns internal item objects, which can be returned from GraphQL resolvers. {% /well %} {% /related-content %} --- # Get Context Source: https://keystonejs.com/docs/context/get-context Keystone's command line interface is helpful, but sometimes you don't want to startup the HTTP server, or kickoff any build processes. Maybe you need to insert data into the database, maybe you want to use Keystone with a custom protocol or a tiny REST API. _Or maybe you want to write unit tests._ If you have previously run `keystone build` or `keystone dev`, then you can use `getContext`. If you change your configuration, you should [rebuild your project](../guides/cli) before using `getContext`. Using the `getContext` function does not use the typical Keystone entry point - it is a function and only requires that your Prisma client has been built and can be provided as a parameter with your Keystone configuration. ```typescript import { getContext } from '@keystone-6/core/context' import config from './keystone.ts' import * as PrismaModule from './generated/prisma/client.ts' const context = getContext(config, PrismaModule) // ... whatever you need ``` {% hint kind="tip" %} For inspiration, see [the script example project](https://github.com/keystonejs/keystone/tree/main/examples/script), which uses Node.js's built-in TypeScript support to seed the database with some data. Running the example requires Node.js 22.18 or newer and `"allowImportingTsExtensions": true` in your `tsconfig.json. You can also use tools like [tsx](https://github.com/esbuild-kit/tsx) if you don't want to use imports with extensions that Node's built-in TypeScript support requires or you want to use syntax that Node's built-in TypeScript support doesn't support. {% /hint %} A context created in this way does not have an implicit `session`, nor is it a `sudo()` context. For more information about how to use a context, please see the [overview](./overview). {% hint kind="warn" %} The `context` returned from `getContext` is not a global - every time you instantiate it, you are instantiating a Prisma Client. Without careful usage, you may encounter the warnings that there are too many instances of your Prisma Client. {% /hint %} ## Related resources {% related-content %} {% well heading="Example Project: Script" href="https://github.com/keystonejs/keystone/tree/main/examples/script" target="_blank" %} Shows you how to write tests against the GraphQL API to your Keystone system. Builds on the Authentication example project. Shows you how to write code to interact with Keystone without using the standard `@keystone-6/core/scripts/cli` tools. {% /well %} {% well heading="Query API Reference" href="/docs/context/query" %} A programmatic API for running CRUD operations against your GraphQL API. For each list in your system you get an API at `context.query.`. {% /well %} {% well heading="Context API Reference" href="/docs/context/overview" %} The API for run-time functionality in your Keystone system. Use it to write business logic for access control, hooks, testing, GraphQL schema extensions, and more. {% /well %} {% /related-content %} --- # Query Source: https://keystonejs.com/docs/context/query The Query API provides a programmatic API for running CRUD operations against your GraphQL API. For each list in your system the following API is available at `context.query.`. ``` { findOne({ where: { id }, query }), findMany({ where, take, skip, orderBy, query }), count({ where }), createOne({ data, query }), createMany({ data, query }), updateOne({ where: { id }, data, query }), updateMany({ data, query }), deleteOne({ where: { id }, query }), deleteMany({ where, query }), } ``` The arguments to these functions closely correspond to their equivalent GraphQL APIs, making it easy to switch between the programmatic API and the GraphQL API. The `query` argument (which defaults to `'id'`), is a string which indicates which fields should be returned by the operation. The functions in the API work by directly executing queries and mutations against your GraphQL API. ### findOne ```typescript const user = await context.query.User.findOne({ where: { id: '...' }, query: 'id name posts { id title }', }); ``` ### findMany ```typescript const users = await context.query.User.findMany({ where: { name: { startsWith: 'A' } }, take: 10, skip: 20, orderBy: [{ name: 'asc' }], query: 'id name posts { id title }', }); ``` ### count ```typescript const count = await context.query.User.count({ where: { name: { startsWith: 'A' } }, }); ``` ### createOne ```typescript const user = await context.query.User.createOne({ data: { name: 'Alice', posts: { create: [{ title: 'My first post' }] }, }, query: 'id name posts { id title }', }); ``` ### createMany ```typescript const users = await context.query.User.createMany({ data: [ { name: 'Alice', posts: { create: [{ title: 'Alices first post' }] }, }, { name: 'Bob', posts: { create: [{ title: 'Bobs first post' }] }, }, ], query: 'id name posts { id title }', }); ``` ### updateOne ```typescript const user = await context.query.User.updateOne({ where: { id: '...' }, data: { name: 'Alice', posts: { create: [{ title: 'My first post' }] }, }, query: 'id name posts { id title }', }); ``` ### updateMany ```typescript const users = await context.query.User.updateMany({ data: [ { where: { id: '...' }, data: { name: 'Alice', posts: { create: [{ title: 'Alices first post' }] }, }, }, { where: { id: '...' }, data: { name: 'Bob', posts: { create: [{ title: 'Bobs first post' }] }, }, }, ], query: 'id name posts { id title }', }); ``` ### deleteOne ```typescript const user = await context.query.User.deleteOne({ where: { id: '...' }, query: 'id name posts { id title }', }); ``` ### deleteMany ```typescript const users = await context.query.User.deleteMany({ where: [{ id: '...' }, { id: '...' }], query: 'id name posts { id title }', }); ``` ## Related resources {% related-content %} {% well heading="Context API Reference" href="/docs/context/overview" %} The API for run-time functionality in your Keystone system. Use it to write business logic for access control, hooks, testing, GraphQL schema extensions, and more. {% /well %} {% well heading="DB API Reference" href="/docs/context/db-items" %} The API for running CRUD operations against the internal GraphQL resolvers in your system. It returns internal item objects, which can be returned from GraphQL resolvers. {% /well %} {% /related-content %} --- # Database Source: https://keystonejs.com/docs/context/db-items The database API provides a programmatic API for running CRUD operations against the internal GraphQL resolvers in your system. Importantly, this API bypasses the GraphQL Server itself, instead invoking the resolver functions directly. The return values of this API are **internal item** objects, which are suitable to be returned from GraphQL resolvers. This API executes the [`access control`](../guides/auth-and-access-control) rules and [`hooks`](../config/hooks) defined in your system. To bypass these, you can directly use the Prisma Client at [`context.prisma`](../context/overview#database-access). For each list in your system the following API is available at `context.db.`. ``` { findOne({ where: { id } }), findMany({ where, take, skip, orderBy }), count({ where }), createOne({ data }), createMany({ data }), updateOne({ where, data }), updateMany({ data }), deleteOne({ where }), deleteMany({ where }), } ``` The arguments to these functions approximate their equivalent [GraphQL APIs](../graphql/overview). ### findOne ```typescript const user = await context.db.User.findOne({ where: { id: '...' }, }); ``` ### findMany ```typescript const users = await context.db.User.findMany({ where: { name: { startsWith: 'A' } }, take: 10, skip: 20, orderBy: [{ name: 'asc' }], }); ``` ### count ```typescript const count = await context.db.User.count({ where: { name: { startsWith: 'A' } }, }); ``` ### createOne ```typescript const user = await context.db.User.createOne({ data: { name: 'Alice', posts: { create: [{ title: 'My first post' }] }, }, }); ``` ### createMany ```typescript const users = await context.db.User.createMany({ data: [ { name: 'Alice', posts: { create: [{ title: 'Alices first post' }] }, }, { name: 'Bob', posts: { create: [{ title: 'Bobs first post' }] }, }, ], }); ``` ### updateOne ```typescript const user = await context.db.User.updateOne({ where: { id: '...' }, data: { name: 'Alice', posts: { create: [{ title: 'My first post' }] }, }, }); ``` ### updateMany ```typescript const users = await context.db.User.updateMany({ data: [ { where: { id: '...' }, data: { name: 'Alice', posts: { create: [{ title: 'Alices first post' }] }, }, }, { where: { id: '...' }, data: { name: 'Bob', posts: { create: [{ title: 'Bobs first post' }] }, }, }, ], }); ``` ### deleteOne ```typescript const user = await context.db.User.deleteOne({ where: { id: '...' }, }); ``` ### deleteMany ```typescript const users = await context.db.User.deleteMany({ where: [{ id: '...' }, { id: '...' }], }); ``` ## Related resources {% related-content %} {% well heading="Query API Reference" href="/docs/context/query" %} A programmatic API for running CRUD operations against your GraphQL API. For each list in your system you get an API at `context.query.`. {% /well %} {% well heading="Context API Reference" href="/docs/context/overview" %} The API for run-time functionality in your Keystone system. Use it to write business logic for access control, hooks, testing, GraphQL schema extensions, and more. {% /well %} {% /related-content %} --- # GraphQL Overview Source: https://keystonejs.com/docs/graphql/overview Keystone generates a CRUD (create, read, update, delete) GraphQL API based on the [schema](../config/lists) definition provided in the system [config](../config/config). ## Using the API By default Keystone serves your GraphQL endpoint at `/api/graphql`. When `NODE_ENV=production` is not set, by default Keystone serves the [GraphQL playground](https://github.com/graphql/graphql-playground), an in-browser GraphQL IDE for debugging and exploring the API and GraphQL schema that Keystone built. With the default configuration, the GraphQL playground and API endpoint is served on the path [`http://localhost:3000/api/graphql`](http://localhost:3000/api/graphql). The URL of the GraphQL API is often configured as an environment variable when building or developing your frontend application, when [initializing your GraphQL instance](https://www.apollographql.com/docs/react/get-started/#2-initialize-apolloclient) (example is Apollo, but any GraphQL client is OK). For example: ```js const client = new ApolloClient({ uri: process.env.APOLLO_CLIENT_GRAPHQL_URI || 'http://localhost:3000/api/graphql', cache: new InMemoryCache() }); ``` If you don't like the default path you can control where the GraphQL API and playground are published by setting `config.graphql.path` in the [Keystone configuration](https://keystonejs.com/docs/config/config#graphql). For security through obscurity, the playground and [introspection](https://graphql.org/learn/introspection/) is disabled when running Keystone with `NODE_ENV=production`. You can modify this behaviour using the `config.graphql.playground` and `config.graphql.apolloConfig` options. For example, to disable these features irrespective of the `NODE_ENV` environment variables, add this to your Keystone config: `graphql: { playground: false, apolloConfig: { introspection: false } }`. ## Example Consider the following system definition: ```typescript import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; export default config({ lists: { User: list({ fields: { name: text() } }), }, /* ... */ }); ``` This system will generate the following GraphQL API. **Note:** The names and types of the generated queries and mutations are based on the names of the lists and fields in the system config. ```graphql type Query { users( where: UserWhereInput! = {} orderBy: [UserOrderByInput!]! = [] take: Int skip: Int! = 0 ): [User!] user(where: UserWhereUniqueInput!): User usersCount(where: UserWhereInput! = {}): Int } type User { id: ID! name: String } input UserWhereUniqueInput { id: ID } input UserWhereInput { AND: [UserWhereInput!] OR: [UserWhereInput!] NOT: [UserWhereInput!] id: IDFilter name: StringNullableFilter } input IDFilter { equals: ID in: [ID!] notIn: [ID!] lt: ID lte: ID gt: ID gte: ID not: IDFilter } input StringNullableFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String mode: QueryMode not: NestedStringNullableFilter } enum QueryMode { default insensitive } input NestedStringNullableFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String not: NestedStringNullableFilter } input UserOrderByInput { id: OrderDirection name: OrderDirection } enum OrderDirection { asc desc } type Mutation { createUser(data: UserCreateInput!): User createUsers(data: [UserCreateInput!]!): [User] updateUser(where: UserWhereUniqueInput!, data: UserUpdateInput!): User updateUsers(data: [UserUpdateArgs!]!): [User] deleteUser(where: UserWhereUniqueInput!): User deleteUsers(where: [UserWhereUniqueInput!]!): [User] } input UserUpdateInput { name: String } input UserUpdateArgs { where: UserWhereUniqueInput! data: UserUpdateInput! } input UserCreateInput { name: String } ``` ### Queries #### `user` ```graphql type Query { user(where: UserWhereUniqueInput!): User } type User { id: ID! name: String } input UserWhereUniqueInput { id: ID } ``` If a field has `isIndexed: 'unique'`, or the list has a 1-to-1 relationship, a `WhereUniqueInput` type will be added to the GraphQL schema output: ```graphql input UserWhereUniqueInput { id: ID email: String profile: ProfileWhereUniqueInput } ``` #### `users` ```graphql type Query { users( where: UserWhereInput! = {} orderBy: [UserOrderByInput!]! = [] take: Int skip: Int! = 0 ): [User!] } type User { id: ID! name: String } input UserWhereInput { AND: [UserWhereInput!] OR: [UserWhereInput!] NOT: [UserWhereInput!] id: IDFilter name: StringNullableFilter } input IDFilter { equals: ID in: [ID!] notIn: [ID!] lt: ID lte: ID gt: ID gte: ID not: IDFilter } input StringNullableFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String mode: QueryMode not: NestedStringNullableFilter } enum QueryMode { default insensitive } input NestedStringNullableFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String not: NestedStringNullableFilter } input UserOrderByInput { id: OrderDirection name: OrderDirection } enum OrderDirection { asc desc } ``` #### `usersCount` ```graphql type Query { usersCount(where: UserWhereInput! = {}): Int } input UserWhereInput { AND: [UserWhereInput!] OR: [UserWhereInput!] NOT: [UserWhereInput!] id: IDFilter name: StringNullableFilter } input IDFilter { equals: ID in: [ID!] notIn: [ID!] lt: ID lte: ID gt: ID gte: ID not: IDFilter } input StringNullableFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String mode: QueryMode not: NestedStringNullableFilter } enum QueryMode { default insensitive } input NestedStringNullableFilter { equals: String in: [String!] notIn: [String!] lt: String lte: String gt: String gte: String contains: String startsWith: String endsWith: String not: NestedStringNullableFilter } ``` ### Mutations #### `createUser` ```graphql type Mutation { createUser(data: UserCreateInput!): User } input UserCreateInput { name: String } type User { id: ID! name: String } ``` #### `createUsers` ```graphql type Mutation { createUsers(data: [UserCreateInput!]!): [User] } input UserCreateInput { name: String } type User { id: ID! name: String } ``` #### `updateUser` ```graphql type Mutation { updateUser(where: UserWhereUniqueInput!, data: UserUpdateInput!): User } input UserWhereUniqueInput { id: ID } input UserUpdateInput { name: String } type User { id: ID! name: String } ``` #### `updateUsers` ```graphql type Mutation { updateUsers(data: [UserUpdateArgs!]!): [User] } input UserUpdateArgs { where: UserWhereUniqueInput! data: UserUpdateInput! } input UserWhereUniqueInput { id: ID } input UserUpdateInput { name: String } type User { id: ID! name: String } ``` #### `deleteUser` ```graphql type Mutation { deleteUser(where: UserWhereUniqueInput!): User } input UserWhereUniqueInput { id: ID } type User { id: ID! name: String } ``` #### `deleteUsers` ```graphql type Mutation { deleteUsers(ids: [UserWhereUniqueInput!]!): [User] } input UserWhereUniqueInput { id: ID } type User { id: ID! name: String } ``` ## Errors The Keystone GraphQL API is powered by Apollo Server. When something goes wrong with a query or mutation, one or more errors will be returned in the `errors` array returned to the [GraphQL client](https://www.apollographql.com/docs/react/data/error-handling/). Keystone provides [custom errors](https://www.apollographql.com/docs/apollo-server/data/errors/#custom-errors) where possible, including custom error codes and messages. These error codes and messages can be used to provide useful feedback to users, and also to help identify possible bugs in your system. The following error codes can be returned from the Keystone GraphQL API. - `KS_USER_INPUT_ERROR`: The input to the operation is syntactically correct GraphQL, but the values provided are invalid. E.g, an `orderBy` input without any keys. - `KS_ACCESS_DENIED`: The operation is not allowed because either an [Access Control](../config/access-control) rule prevents it, or the item does not exist. - `KS_FILTER_DENIED`: The filter or ordering operation is not allowed because of [`isFilterable` or `isOrderable`](../fields/overview#common-configuration) rules. - `KS_VALIDATION_FAILURE`: The operation is not allowed because of a [validation](../guides/hooks#validating-inputs) rule. - `KS_LIMITS_EXCEEDED`: The user has exceeded some query limits. E.g, a `take` input [that is too high](../config/lists#graphql). - `KS_EXTENSION_ERROR`: An error was thrown while excuting a system extension function, such as a hook or an access control function. - `KS_ACCESS_RETURN_ERROR`: An invalid value was returned from an access control function. - `KS_RESOLVER_ERROR`: An error occurred while resolving the input for a field. - `KS_RELATIONSHIP_ERROR`: An error occurred while resolving the input relationship field. - `KS_PRISMA_ERROR`: An error occurred while running a Prisma client operation. Prisma error details may be stripped from GraphQL responses for security. You can use `graphql.apolloConfig.formatError` to customise this behaviour. > A note on `KS_ACCESS_DENIED`: Returning a "not found" error from a mutation like `updateUser({ where: { secretKey: 'abc' } })` but an "access denied" error from `updateUser({ where: { secretKey: 'def' } })` would reveal the existence of a user with the secret key "def". To prevent leaking private information in this way, Keystone will always say "access denied" when you try to perform a mutation on an item that can't be operated on, whether that is because there is no matching record in the database, or there was but the user performing the operation doesn't have access to it. ## Related resources {% related-content %} {% well heading="Lists API Reference" href="/docs/config/lists" %} The API to configure your options used with the `list()` function. {% /well %} {% well heading="Config API Reference" href="/docs/config/config" %} The API to configure all the parts of your Keystone system. {% /well %} {% /related-content %} --- # GraphQL Query Filters Source: https://keystonejs.com/docs/graphql/filters Each field type provides its own set of filters which can be used with [queries](./overview#users). This page lists all the filters available for each field type. For more details on how to use filters in queries please consult to the [GraphQL Queries - Filters](../guides/filters) guide. ## Scalar types ### checkbox {% table %} - **Filter name** - **Type** - **Description** --- - `equals` - `Boolean` - Equals --- - `not` - `BooleanNullableFilter` - Does not match the inner filter {% /table %} ### integer {% table %} - **Filter name** - **Type** - **Description** --- - `equals` - `Int` - Equals --- - `lt` - `Int` - Less than --- - `lte` - `Int` - Less than or equal --- - `gt` - `Int` - Greater than --- - `gte` - `Int` - Greater than or equal --- - `in` - `[Int!]` - Is in the array --- - `notIn` - `[Int!]` - Is not in the array --- - `not` - `IntNullableFilter` - Does not match the inner filter {% /table %} ### bigInt {% table %} - **Filter name** - **Type** - **Description** --- - `equals` - `BigInt` - Equals --- - `lt` - `BigInt` - Less than --- - `lte` - `BigInt` - Less than or equal --- - `gt` - `BigInt` - Greater than --- - `gte` - `BigInt` - Greater than or equal --- - `in` - `[BigInt!]` - Is in the array --- - `notIn` - `[BigInt!]` - Is not in the array --- - `not` - `BigIntNullableFilter` - Does not match the inner filter {% /table %} ### json The `json` field type does not support filters. ### float {% table %} - **Filter name** - **Type** - **Description** --- - `equals` - `Float` - Equals --- - `lt` - `Float` - Less than --- - `lte` - `Float` - Less than or equal --- - `gt` - `Float` - Greater than --- - `gte` - `Float` - Greater than or equal --- - `in` - `[Float!]` - Is in the array --- - `notIn` - `[Float!]` - Is not in the array --- - `not` - `FloatNullableFilter` - Does not match the inner filter {% /table %} ### password {% table %} - **Filter name** - **Type** - **Description** --- - `isSet` - `Boolean` - A value is set {% /table %} ### select - If the `type` is `string`(the default), the same filters as `text` will be available. - If the `type` is `integer`, the same filters as `integer` will be available. - If the `type` is `enum`, the following filters will be available: | **Filter name** | **Type** | **Description** | | --------------- | ---------- | ------------------- | | `equals` | `ListKeyFieldKeyType` | Equals | | `in` | `[ListKeyFieldKeyType!]` | Is in the array | | `notIn` | `[ListKeyFieldKeyType!]` | Is not in the array | | `not` | `ListKeyFieldKeyTypeNullableFilter` | Does not match the inner filter | ### text {% table %} - **Filter name** - **Type** - **Description** - **Notes** --- - `equals` - `String` - Equals - --- - `lt` - `String` - Less than - --- - `lte` - `String` - Less than or equal - --- - `gt` - `String` - Greater than - --- - `gte` - `String` - Greater than or equal - --- - `contains` - `String` - Contains - [1] --- - `startsWith` - `String` - Starts with - [1] --- - `endsWith` - `String` - Ends with - [1] --- - `in` - `[String!]` - Is in the array - --- - `notIn` - `[String!]` - Is not in the array - --- - `mode` - `QueryMode` (`default` or `insensitive`) - Whether the filters should be case insensitive or not - [2] --- - `not` - `NestedStringNullableFilter` - Does not match the inner filter - {% /table %} #### Notes - [1] Will follow the setting of the `mode` on `postgresql` and will be case insensitive but only for ASCII characters on `sqlite` - [2] `postgresql` only ### timestamp {% table %} - **Filter name** - **Type** - **Description** --- - `equals` - `String` - Equals --- - `lt` - `String` - Less than --- - `lte` - `String` - Less than or equal --- - `gt` - `String` - Greater than --- - `gte` - `String` - Greater than or equal --- - `in` - `[String!]` - Is in the array --- - `notIn` - `[String!]` - Is not in the array --- - `not` - `DateTimeNullableFilter` - Does not match the inner filter {% /table %} ## Relationship type ### relationship #### many: true {% table %} - **Filter name** - **Type** - **Description** --- - `every` - `FooWhereInput` - All related items match the nested filter --- - `some` - `FooWhereInput` - Some related items match the nested filter --- - `none` - `FooWhereInput` - No related items match the nested filter {% /table %} #### many: false {% table %} - **Filter name** - **Type** - **Description** --- - `foo` - `FooWhereInput` - Matches the nested filter {% /table %} ## Virtual type ### virtual The `virtual` field type does not support filters. ## File types ### file The `file` field type does not support filters. ### image The `image` field type does not support filters. ## Related resources {% related-content %} {% well heading="Query Filters Guide" grad="grad1" href="/docs/guides/filters" target="" %} Query filters are an integral part of Keystone’s powerful GraphQL APIs. This guide will show you how to use filters to get the data you need from your system. {% /well %} {% /related-content %} --- # Privacy Policy (Telemetry) Source: https://keystonejs.com/docs/reference/telemetry Keystone 6 is software developed and maintained by Thinkmill Labs Pty Ltd and the open source community, and this privacy policy is on behalf of Thinkmill Labs and in relation to Keystone, and to the extent applicable, similar software under the Keystone GitHub organisation. This page describes how we collect and hold **anonymous** telemetry data, the rationale for why we are collecting this information, when it is collected, and how **you can opt-out**. Unless you opt out, you consent to us collecting, holding, analysing and using the telemetry data in accordance with this policy, which may be updated from time to time. Please note that Keystone currently uses [NextJS](https://nextjs.org/) and [Prisma](https://www.prisma.io/) as software dependencies, and each of these projects include their own telemetry with their own respective privacy policies. You can learn more about them by visiting the following: - [https://nextjs.org/telemetry](https://nextjs.org/telemetry) - [https://www.prisma.io/docs/concepts/more/telemetry](https://www.prisma.io/docs/concepts/more/telemetry) --- ## Why do we collect and hold telemetry? Keystone only collects telemetry that we reasonably require for the continued development of the software, and, to the extent applicable, we do so in accordance with the Australian Privacy Principles. Telemetry helps us learn about how Keystone is being used by developers and projects that may not be actively participating on our GitHub, Twitter and Slack. The questions we want to currently answer using telemetry are the following: - How many active developers are using Keystone, compared to how many are active in the community, - How complex are projects that use Keystone, - What field types and `@keystone-6/` or community nominated packages are being used by developers, and - What operating systems and node versions are being used by the community? This information will help Thinkmill Labs prioritise future features or maintenance efforts for ongoing development. --- ## When is telemetry collected? Keystone only collects telemetry when you, the developer, explicitly start the software using `keystone dev`, or by using the `cli` function with an `argv` value of `dev`, as exposed by our `@keystone-6/core/scripts/cli` export. There are four scenarios when running `keystone dev`, where we **do not collect** telemetry: - The first time running `keystone dev`, when displaying the telemetry notice, or - If the `CI` environment variable is set (to anything), or - If the `NODE_ENV` environment variable is set to `"production"`, or - If you have opted out - see [How to opt-out below](#how-to-opt-out) In any of these scenarios, telemetry will be disabled and prevented from sending any information. --- ## What information is collected? {% hint kind="warn" %} ⚠️ The exact structure and contents of the telemetry information collected may be subject to change when the software is updated. Developers should watch [our releases on GitHub](https://github.com/keystonejs/keystone/releases) for any changes in what telemetry information is reported by the Software. Any material increase in the amount of information collected will result in us re-notifying developers of that change the next time they run `keystone dev`; and updating this page. {% /hint %} Keystone collects telemetry information in the form of two different types of data reports: - Information about the device running `keystone dev`, and - Information about the project’s configuration We refer to these two different reports, as “device telemetry” and “project telemetry” respectively. These reports are forwarded to [https://telemetry.keystonejs.com/](https://telemetry.keystonejs.com/), and are reported separately to minimize any correlation between them insofar as the timing and grouping of that data, that an otherwise combined report may have. We are collecting these two reports for different reasons, and thus have no need to associate them. We additionally record a timestamp of the time that the report is received by the server at [https://telemetry.keystonejs.com](https://telemetry.keystonejs.com/). The type of report and the version of the report is determined by the location the report is forwarded to. #### Device Telemetry The type of information contained within a device telemetry report is currently: - The last date you used `keystone dev`, and - The node major version number, and - The name of your operating system A device telemetry report is formatted as JSON and currently looks like: ```json { "lastSentDate": "2024-11-23", "os": "darwin", "node": "20" } ``` Device telemetry reports are forwarded to [https://telemetry.keystonejs.com/3/device](https://telemetry.keystonejs.com/3/device). #### Project Telemetry The type of information contained within a project telemetry report is currently: - The last date you used `keystone dev` for this project, and - The resolved package versions of any `@keystone-6/` (or community nominated packages) packages used by this project, and - The database type used by the project, - The number of lists used by this project, and - The name and number of field types used by this project A project telemetry report is formatted as JSON and currently looks like: ```json { "lastSentDate": "2025-07-28", "packages": { "@keystone-6/auth": "8.0.1", "@keystone-6/core": "6.1.0", "@keystone-6/document-renderer": "1.1.2", "@keystone-6/fields-document": "5.0.2" }, "database": "postgresql", "lists": 3, "fields": { "unknown": 1, "@keystone-6/text": 5, "@keystone-6/timestamp": 2, "@keystone-6/checkbox": 1 } } ``` Project telemetry reports are forwarded to [https://telemetry.keystonejs.com/3/project](https://telemetry.keystonejs.com/3/project). --- ## Will we share this information? The telemetry information as collected will be used and analysed by Thinkmill Labs to answer the aforementioned questions. For these purposes, we might use 3rd party services. Where possible, we want to share this information with the open-source community surrounding Keystone. Thinkmill Labs respects your privacy, and as such, any information that is shared will only be shared in an aggregate form. We do not sell this data, and we don’t intend to. Thinkmill Labs will make its best effort to remove any outliers, even in aggregate form, that could be used to identify a particular device or project. --- ## How long will we keep this information? Thinkmill Labs will only keep this information until it can be reduced down to aggregate form to answer the aforementioned questions. For that purpose, **we currently only retain telemetry reports and their respective timestamps for 1 year**. After that period, singular telemetry reports are deleted and only the analysed, aggregate form of the respective data is retained. --- ## How to opt-out? To opt-out of Keystone device and project telemetry for your user profile, you can use the following: ```bash $ keystone telemetry disable ``` If you change your user profile on your device, you will need to run this command again. Alternatively, to disable telemetry for your project in a way that is compatible with your source control, add the following to your project's `keystone.ts` configuration file: ```tsx export default config({ db: { // ... }, lists, // this will opt-out of device and project telemetry, for this project telemetry: false }) ``` If you want to reset your telemetry configuration for your user profile, you can use `keystone telemetry reset`. If you want to opt-in to keystone telemetry for your user profile, you can use `keystone telemetry enable`. Keystone stores your telemetry preferences in a location defined by [env-paths](https://github.com/sindresorhus/env-paths#pathsconfig), an open source library, which currently stores the data in the following locations: | Operating System | Location | | --- | --- | | MacOS | ~/Library/Preferences/keystonejs | | Linux | ~/.config/keystonejs (or $XDG_CONFIG_HOME/keystonejs) | | Windows | %APPDATA%\keystonejs\Config (for example C:\Users\YOUR_USERNAME\AppData\Roaming\keystonejs\Config) | #### Environment Variable You can opt-out of all telemetry by setting the `KEYSTONE_TELEMETRY_DISABLED` environment variable to `'1'` #### Network-wide opt-out If you have a network-wide firewall, you can opt-out of Keystone telemetry by not resolving the following domain: [telemetry.keystonejs.com](https://telemetry.keystonejs.com) --- ## How can I see what is currently configured If you wish to see how telemetry is currently configured for your device or project, you can run `keystone telemetry status`. ## What if I have a complaint or question If you have any questions or concerns about the information that is gathered please contact us by logging a GitHub Issue [https://github.com/keystonejs/keystone](https://github.com/keystonejs/keystone). Alternatively please contact our Privacy Officer by email to [privacy@keystonejs.com](mailto:privacy@keystonejs.com), or by mail to Level 10, 191 Clarence Street, Sydney NSW 2000. For further information about Keystone’s security policy please see [https://github.com/keystonejs/keystone/security/policy](https://github.com/keystonejs/keystone/security/policy) --- # Getting started Source: https://keystonejs.com/docs/getting-started ![A terminal with the output of create-keystone-app](/assets/getting-started/cover.svg) [`create-keystone-app`](https://github.com/keystonejs/keystone/tree/main/packages/create) is a CLI app that makes it easier for you to initiate a Keystone project. It generates the files you need to run the Admin UI and start using the [GraphQL API](/docs/graphql/overview). ## Quick Start ```sh npm create keystone-app@latest cd my-app npm install npm run dev ``` ## Installing a Keystone instance Open your preferred shell and make sure you’re in the folder you want to create your new project in. `create-keystone-app` will generate a new folder with your new Keystone files in it. ### npm ```sh npm create keystone-app@latest ``` ### npx Use npm's [`npx`](https://docs.npmjs.com/cli/v7/commands/npx): ```sh npx create-keystone-app@latest ``` ## Naming your app The CLI will ask you to name your app. Once named, it will create a new SQLite database. {% hint kind="warn" %} You can switch to another database such as PostgreSQL once your project is created, check out the docs on [Database Setup](https://keystonejs.com/docs/config/config#db). {% /hint %} ## Opening your shiny new Admin UI You can now `cd` into the folder that was created for you, install dependencies, and start Keystone: ```sh cd my-app npm install npm run dev ``` This will generate the Admin UI pages via [Next.js](https://nextjs.org/) on . The first time the project starts, it creates an initial user and logs its email address and randomly generated password in the terminal. Use those credentials to sign in. You’ll see a new Keystone Admin UI that comes with two [lists](/docs/config/config#lists). From here you can explore and interact with the data in your system, and understand how Keystone’s schema relates to your GraphQL API which you can explore at . ![The Admin UI of Keystone showing the two lists: User and Posts](/assets/getting-started/adminui.png) ## Output Keystone creates the following files in your newly generated folder. The most important ones are [`keystone.ts`](/docs/config/config) and [`schema.ts`](/docs/config/lists). ```sh . ├── auth.ts # Authentication configuration for Keystone ├── keystone.ts # The main entry file for configuring Keystone ├── node_modules # Your dependencies ├── generated/prisma # Generated Prisma Client (ignored by Git) ├── package.json # Your package.json with three scripts prepared for you ├── package-lock.json # Your npm lock file ├── prisma.config.ts # Prisma CLI datasource and migration configuration ├── README.md # Additional info to help you get started ├── schema.graphql # GraphQL schema (automatically generated by Keystone) ├── schema.prisma # Prisma schema (automatically generated by Keystone) ├── schema.ts # Where you design your data schema └── tsconfig.json # Your typescript config ``` ## Scripts `package.json` includes the following npm scripts you can run locally: - [`dev`](/docs/guides/cli#dev) runs Keystone in **development** mode at . - [`start`](/docs/guides/cli#start) runs Keystone in **production** mode. - [`build`](/docs/guides/cli#build) will build the project and is required to be run before `start`. - [`postinstall`](/docs/guides/cli#postinstall) ensures files that Keystone generates exist and are kept up to date. {% hint kind="tip" %} Read more about the CLI in our [command line guides](/docs/guides/cli). {% /hint %} ## Where to next? - Customise Keystone with the [System Configuration API](./config/config). - Add fields and relationships using [Lists API](/docs/config/lists). - Use a different database with the [Config API](/docs/config/config#db). ## Related resources {% related-content %} {% well heading="Keystone CLI Guide" href="/docs/guides/cli" %} Keystone’s CLI helps you develop, build, and deploy projects. This guide explains all you need to standup a new backend in the terminal. {% /well %} {% well heading="Lists API Reference" href="/docs/config/lists" %} The API to configure your options used with the `list()` function. {% /well %} {% well heading="Config API Reference" href="/docs/config/config" %} The API to configure all the parts of your Keystone system. {% /well %} {% well heading="DB API Reference" href="/docs/config/config#db" %} Configure Keystone to use another database to store data in your system. {% /well %} {% /related-content %} --- # Custom Field Views Source: https://keystonejs.com/docs/guides/custom-field-views {% coming-soon /%} --- # Lesson 1: Installing Keystone Source: https://keystonejs.com/docs/walkthroughs/lesson-1 Learn how to install Keystone, create your first content type, and get an app up and running with an intuitive editing environment. ## Introduction Welcome to the Keystone getting started series! Together we’ll learn how to turn an empty folder into a database-backed Keystone instance with related content types, publishing workflows, password protection, an editing interface, and more. {% hint kind="tip" %} Looking to demo Keystone in under 5 minutes? [Try the quick-start CLI](/docs/getting-started) {% /hint %} This series assumes you have some basic familiarity with: - The **command line** for running Keystone - The **node ecosystem** for installing packages from npm, and running npm scripts - **JavaScript/TypeScript** for programming Keystone. You won't need to use much typescript in this series, however you can still benefit from using Keystone's inbuilt types - **GraphQL** for querying your Keystone content ## Install Keystone Let’s start by setting up a workspace for a new Keystone project. Make a new folder and initialise it: ```bash mkdir keystone-learning cd keystone-learning npm init ``` {% hint kind="tip" %} We’ll be using `npm` for installing packages, but you can use any other package manager you prefer. {% /hint %} Now add Keystone, Prisma, and the SQLite driver adapter: ```bash npm install @keystone-6/core @prisma/adapter-better-sqlite3 @prisma/client better-sqlite3 prisma ``` ## Configure Keystone Keystone looks for a file named `keystone.ts` (or `keystone.js`) at the project root to handle configuration needs. Let's go ahead and set one up using TypeScript: ```js // keystone.ts export default {} ``` And add TypeScript as a dependency: ```bash npm install typescript ``` {% hint kind="tip" %} If you’d rather use JavaScript, make a `keystone.js` file and skip the TypeScript installation step above. {% /hint %} Your folder structure should now look like this: ```sh . ├── node_modules # Dependencies ├── keystone.ts # Keystone config ├── package.json # With Keystone and TypeScript as dependencies ├── prisma.config.ts # Prisma CLI and migration configuration └── package-lock.json # Your npm lock file ``` Create `prisma.config.ts` alongside `keystone.ts` so the Prisma CLI knows where to find the generated schema and database: ```ts import { defineConfig } from 'prisma/config' export default defineConfig({ schema: 'schema.prisma', migrations: { path: 'migrations' }, datasource: { url: 'file:./keystone.db' }, }) ``` We now need to configure `keystone.ts` with two parts to get our project running: - [`db`](/docs/config/config#db) to define a database configuration - [`list`](/docs/config/lists) to define the shape of the information we put in that database ### Add a Database We’ll use [SQLite](/docs/config/config#sqlite) in this project to keep things simple, but you can also use [Postgres](/docs/config/config#postgresql). The minimum config for SQLite looks like this: ```ts import { config } from '@keystone-6/core' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists: {}, // ... }) ``` The adapter connects Keystone's runtime to the local `./keystone.db` database. The matching URL in `prisma.config.ts` is used by Prisma CLI commands. {% hint kind="tip" %} Keystone uses [Prisma](https://www.prisma.io/) for database access and development-time schema pushes. For production, manage migration files with the [Prisma CLI](/docs/guides/cli#working-with-migrations). {% /hint %} ### Create your first List Now that we have a database configured, let’s connect some content to it! We’re going to build a simple blog with **users** and **posts**. Let’s start with the `User` list using [`text`](/docs/fields/text) fields for their `name` and `email`: ```js{3,11-18}[6-10] import { config, list } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { allowAll } from '@keystone-6/core/access'; import { text } from '@keystone-6/core/fields'; export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists: { User: list({ access: allowAll, fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), }, }), }, }); ``` **List** names are derived from the keys in the `lists` object. For example, the `User` key becomes the name for the respective `User` list. This key will be used in Admin UI for the list’s default display name, and in Keystone’s auto-generated GraphQL API. **Field** names are derived from the keys in the [`fields`](/docs/fields/overview) object. Like lists, they’ll be used in Admin UI for field label defaults, and in the GraphQL API. We've added validation to both our fields to say that they are required, and declared that emails must be unique, so there can only be one user with each email. {% hint kind="tip" %} **Admin UI** is the name of Keystone’s user friendly editing environment. Accessible from a browser, it’s the place where we’ll add and update content that will be stored in the database. {% /hint %} ## Run Keystone We now have everything we need to start Keystone, so let’s do just that: ```bash npx keystone dev ``` In a few seconds your terminal will provide you with you a link to the Keystone Admin UI at [http://localhost:3000](http://localhost:3000) ![Terminal dialog showing successful Keystone startup](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-1/keystone-startup.png) Head on over to [http://localhost:3000/users](http://localhost:3000/users) where you can create your first user with a `name` and `email`: ![Adding a user record in Keystone Admin UI](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-1/first-user-creation.gif) **Hurrah! You now have Keystone up and running** {% emoji symbol="🎉" alt="Party Popper" /%} Next up, we’ll level-up our blog starter with a `post` list and connect it to our users. ## Next lesson {% related-content %} {% well heading="Lesson 2: Relating things" grad="grad1" href="/docs/walkthroughs/lesson-2" target="" %} Connect two content types and learn how to configure the appearance of field inputs {% /well %} {% /related-content %} --- # Lesson 2: Creating relationships Source: https://keystonejs.com/docs/walkthroughs/lesson-2 Learn how to connect two content types to each other and configure how you make those connections in Admin UI. ## Where we left off In the [first lesson](/docs/walkthroughs/lesson-1) we got our Keystone blog project up and running with a database and `user` list: ```js // keystone.ts import { config, list } from '@keystone-6/core' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { allowAll } from '@keystone-6/core/access' import { text } from '@keystone-6/core/fields' export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists: { User: list({ access: allowAll, fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), }, }), }, }) ``` We’re now going to create a post list type, and connect it to users with relationship fields. ## Make a Lists object Before we define fields for a `post` type, let's pull lists out into its own object so it's easier to reason about going forward: ```js{6-14,23} import { config, list } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { allowAll } from '@keystone-6/core/access'; import { text } from '@keystone-6/core/fields'; const lists = { User: list({ access: allowAll, fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), }, }), }; export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }); ``` {% hint kind="tip" %} **Protip**: you could also move lists into its own file, and import it into `keystone.ts` {% /hint %} ## Create a Post list To create a post type we add a second `Post` key to the lists object. Let’s add another `text` field for the post’s `title` to begin with: ```js{14-19}[1-5,22-30] import { config, list } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { allowAll } from '@keystone-6/core/access'; import { text } from '@keystone-6/core/fields'; const lists = { User: list({ access: allowAll, fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), }, }), Post: list({ access: allowAll, fields: { title: text(), }, }), }; export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }); ``` Let's check that this works. Boot up our Keystone app and have a look: ![Admin UI showing create screen for a blog post](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-2/my-first-post.png) {% hint kind="warn" %} We’re skipping over the "content" field for the time being to keep this lesson focused on relationships. We’ll come back to it in [Lesson 5](/docs/walkthroughs/lesson-5). {% /hint %} ## Connect Users with Posts Now that we have two lists, let's make a relationship between them. When deciding how to relate lists it's helpful to consider: - Should a user be able to create **one** or **many** posts? - Should a post be attributable to **one** or **many** users? Let’s say that a post can be associated with only one user, but a user can have many posts. To create this relationship, we will add a [relationship](/docs/guides/relationships) field to each list that defines their connection to one another: ```js{4,12,19}[23-32] import { config, list } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { allowAll } from '@keystone-6/core/access'; import { text, relationship } from '@keystone-6/core/fields'; const lists = { User: list({ access: allowAll, fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), }, }), Post: list({ access: allowAll, fields: { title: text(), author: relationship({ ref: 'User.posts' }), }, }), }; export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }); ``` The `relationship` references the name for the `field` of the list it is relating to make it [two-sided](/docs/guides/relationships#two-sided), so the `author` field relates to `User.posts`, while the `posts` field relates to `Post.author`. {% hint kind="tip" %} Naming relationships can be important if lists have multiple relationships. If, for example, we wanted to let users also 'like' posts, we could add a second differently named relationship field between `User` and `Post` called 'likes'. {% /hint %} Let's open the Admin UI and create a post: ![Admin UI showing creation of a nerw post, and relationship to a user from the post ](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-2/my-first-relationship.gif) ## Configure a field’s appearance In our current schema, the `author` field provides a `select` input to connect a user to the post. This is the default `displayMode` for this field type. In Keystone it's possible to change that display to suit your needs. This is achieved with a field's `ui` option. Each field comes with different `ui` options that you can explore. for the `author` field we’ll make a few changes to improve the editing experience for Admin UI users: ```js{22-28}[2-15,32-41] //keystone.ts import { config, list } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { allowAll } from '@keystone-6/core/access'; import { text, relationship } from '@keystone-6/core/fields'; const lists = { User: list({ access: allowAll, fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), }, }), Post: list({ access: allowAll, fields: { title: text(), author: relationship({ ref: 'User.posts', ui: { displayMode: 'cards', cardFields: ['name', 'email'], inlineEdit: { fields: ['name', 'email'] }, linkToItem: true, inlineCreate: { fields: ['name', 'email'] }, }, }), }, }), }; export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }); ``` We've made a few changes here, so let's break them down: - The `displayMode` of the relationship is shown as `cards` - We've made sure that the card links to fields in the `user` list with `linkToItem` - We made the user attributes of `name` and `email` editable inline with `inlineEdit` With a dash of extra config we’ve created a very different editing experience where user data can be altered directly in the post form: ![Admin UI showing create screen for a blog post](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-2/relationship-edit-cards-display.gif) ## What we have now Our app has a new `post` list with a title and a link to `users` via the `author` field that you can edit inline with the `cards` UI display mode. Admin UI editors can also create posts from the `user` form with a two-way relationship. ```ts // keystone.ts import { config, list } from '@keystone-6/core' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { allowAll } from '@keystone-6/core/access' import { text, relationship } from '@keystone-6/core/fields' const lists = { User: list({ access: allowAll, fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), }, }), Post: list({ access: allowAll, fields: { title: text(), author: relationship({ ref: 'User.posts', ui: { displayMode: 'cards', cardFields: ['name', 'email'], inlineEdit: { fields: ['name', 'email'] }, linkToItem: true, inlineCreate: { fields: ['name', 'email'] }, }, }), }, }), } export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }) ``` ## Next lesson {% related-content %} {% well heading="Lesson 3: Publishing workflows" grad="grad1" href="/docs/walkthroughs/lesson-3" target="" %} Support publishing needs with Keystone's `select` and `timestamp` fields {% /well %} {% /related-content %} --- # Lesson 3: Publishing workflows Source: https://keystonejs.com/docs/walkthroughs/lesson-3 Learn how to create a publishing workflow to your app using Keystons’s `select` and `timestamp` fields. ## Where we left off In the [last lesson](/docs/walkthroughs/lesson-1) we added a `post` list to our blog app and setup our first connection between posts and users with Keystone’s relationship field. Here's the code: ```ts // keystone.ts import { list, config } from '@keystone-6/core' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { text, relationship } from '@keystone-6/core/fields' const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), }, }), Post: list({ fields: { title: text(), author: relationship({ ref: 'User.posts', ui: { displayMode: 'cards', cardFields: ['name', 'email'], inlineEdit: { fields: ['name', 'email'] }, linkToItem: true, inlineCreate: { fields: ['name', 'email'] }, }, }), }, }), } export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }) ``` We’re now going to extend our Keystone schema to support publishing needs. ## Build a publishing workflow Our publishing workflow needs the ability to reflect: - **When** the post should be published - The post’s **status** – whether it’s still being drafted, or is ready for publishing These will give us what we need to conditionally display and order posts in a frontend app. ### Add a publish date Keystone’s [`timestamp`](/docs/fields/timestamp) field will let editors associate a date and time with the post: ```ts{3,16}[6-12,18-25,30-500] import { list, config } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { text, timestamp, relationship } from '@keystone-6/core/fields'; const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), author: relationship({ ref: 'User.posts', ui: { displayMode: 'cards', cardFields: ['name', 'email'], inlineEdit: { fields: ['name', 'email'] }, linkToItem: true, inlineCreate: { fields: ['name', 'email'] }, }, }), }, }), }; export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }); ``` ### Add a published status Keystone’s [`select`](/docs/fields/select) field gives editors the ability to choose a value from a predetermined set of options. Let’s use this to capture our post's publish status. To set the the field’s desired values we add `options` to the field’s configuration. They will be the only options available to editors in Admin UI and through Keystone’s auto-generated GraphQL types: ```ts{3,27-32}[6-12,18-25,36-500] import { list, config } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { text, relationship, timestamp, select } from '@keystone-6/core/fields'; const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), author: relationship({ ref: 'User.posts', ui: { displayMode: 'cards', cardFields: ['name', 'email'], inlineEdit: { fields: ['name', 'email'] }, linkToItem: true, inlineCreate: { fields: ['name', 'email'] }, }, }), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], }), }, }), }; export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }); ``` Let’s take a look at how everything comes together in Admin UI: ![Adding a date-time value and sellecting a publsh status in Admin UI](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-3/timestamp-publish-status-1.gif) These defaults give us all the basic functionality we need, but there's room for improvement in the select field: - It would be helpful to present all the `status` options in a way that doesn't make editors click on the input to find out what options are available - Setting a default value for new posts could reduce the likelihood of editors accidentally publishing their work before it's done ### Set a default value Adding a `defaultValue` to the select field’s config will ensure that newly created posts start out with a preferred status. Let's set the default to `draft`: ```js defaultValue: 'draft', ``` ### Implement a segmented control input When a select field only has a few values to choose from, changing the UI's `displayMode` from the default `select` to `segmented-control` will expose all those values in the interface so editor's don't have to click the input in order to know which value to choose. Our limited set of `draft`/`published` options is a perfect fit for this! ```js ui: { displayMode: 'segmented-control' }, ``` This now gives us: ```ts{22-23}[1-4,6-12,14-16,29-500] import { list, config } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { text, timestamp, select, relationship } from '@keystone-6/core/fields'; const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], defaultValue: 'draft', ui: { displayMode: 'segmented-control' }, }), author: relationship({ ref: 'User.posts' }), }, }), }; export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }); ``` Let's put it all together to see the changes: ![Post list type showing "status" select input as segmented control type with default value of "draft"](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-3/status-segmented-control.png) {% hint kind="tip" %} **Protip**: You can use [hooks](/docs/config/hooks) to automatically update `publishedAt` when a post’s status moves from `draft` to `published`. {% /hint %} ## Looking at the GraphQL API We can now query these values using Keystone's [GraphQL API playground](http://localhost:3000/api/graphql) to show all the published posts ```graphql query getAllPosts { posts { title } } ``` And/or posts that have a published status, and were published after a certain date: ```graphql query getPublishedPosts { posts(where: { status: { equals: "published" } }) { title } } ``` ## What we have now We’ve successfully added two new fields to our `post` type that give us the information our system will need to display posts: ```js // keystone.ts import { list, config } from '@keystone-6/core' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { text, timestamp, select, relationship } from '@keystone-6/core/fields' const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], defaultValue: 'draft', ui: { displayMode: 'segmented-control' }, }), author: relationship({ ref: 'User.posts' }), }, }), } export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }) ``` ## Next lesson {% related-content %} {% well heading="Lesson 4: Auth & Sessions" grad="grad1" href="/docs/walkthroughs/lesson-4" target="" %} Add sessions, password protection, and a sign-in screen to your Keystone app {% /well %} {% /related-content %} --- # Lesson 4: Auth & Sessions Source: https://keystonejs.com/docs/walkthroughs/lesson-4 Learn how to add passwords, session data and authentication to your Keystone app. ## Where we left off In the [last lesson](/docs/walkthroughs/lesson-3) we setup a publishing workflow for blog posts and ended up with a keystone.js file that looks like: ```ts //keystone.ts import { list, config } from '@keystone-6/core' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { text, timestamp, select, relationship } from '@keystone-6/core/fields' const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], defaultValue: 'draft', ui: { displayMode: 'segmented-control' }, }), author: relationship({ ref: 'User.posts' }), }, }), } export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }) ``` We're now going to add auth to our app so that different types of users have access to different types of things. While Keystone has very granular permissions controls, which you can read about [here](/docs/guides/auth-and-access-control), this lesson will stay focused on securing our Admin UI behind a password. ## Add the Password field Keystone's [password](/docs/fields/password) field adheres to typical password security recommendations like hashing the password in the database, and masking the password for Admin UI input fields. Let's add a password field to our `User` list so users can authenticate with Keystone: ```ts{3,11}[14-28,30-500] import { list, config } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { password, text, timestamp, select, relationship } from '@keystone-6/core/fields'; const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), password: password({ validation: { isRequired: true } }) }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], defaultValue: 'draft', ui: { displayMode: 'segmented-control' }, }), author: relationship({ ref: 'User.posts' }), }, }), }; export default config({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, }); ``` That's all we need to store secure passwords in our database! ## Add Authentication ### Install the `auth` package Authentication isn't built directly in to Keystone - it's an enhancement you can add on top. To use it in our app we need to add Keystone’s [auth package](https://github.com/keystonejs/keystone/tree/main/packages/auth): ```sh npm install @keystone-6/auth ``` Now that we have the package, let’s create a new file in the root of our project to write our auth config in: ```sh touch auth.ts ``` And add the following code: ```ts // auth.ts import { createAuth } from '@keystone-6/auth' const { withAuth } = createAuth({ listKey: 'User', identityField: 'email', sessionData: 'name', secretField: 'password', }) export { withAuth } ``` This code says: - The `User` list is the list that auth should be applied to - `email` and `password` will be the fields used to log a user in ### Add sessions Having added an authentication method, we need to add a 'session', so that authentication can be kept between refreshes. Also in the auth file, we want to add: ```ts{3,12-500}[4-11] // auth.ts import { createAuth } from '@keystone-6/auth'; import { statelessSessions } from '@keystone-6/core/session'; const { withAuth } = createAuth({ listKey: 'User', identityField: 'email', sessionData: 'name', secretField: 'password', }); let sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'; let sessionMaxAge = 60 * 60 * 24; // 24 hours const session = statelessSessions({ maxAge: sessionMaxAge, secret: sessionSecret, }); export { withAuth, session } ``` ### Import Auth & Sessions to Keystone config Back over in our keystone file, we want to import our `withAuth` function, and our `session` object. `withAuth` will wrap our default export and modify it as a last step in setting up our config. The session is attached to the export. Finally, we need to add an `isAccessAllowed` function to our export so that only users with a valid session can see Admin UI: ```ts{5,33-34,42-45}[8-30] //keystone.ts import { list, config } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { password, text, timestamp, select, relationship } from '@keystone-6/core/fields'; import { withAuth, session } from './auth'; const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), password: password({ validation: { isRequired: true } }) }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], defaultValue: 'draft', ui: { displayMode: 'segmented-control' }, }), author: relationship({ ref: 'User.posts' }), }, }), }; export default config( withAuth({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, session, ui: { isAccessAllowed: (context) => !!context.session?.data, }, }) ); ``` ## Adding an initial user With our new set-up, we'll be locked out of Admin UI if the database has no users. We can use `db.onConnect` to create an initial user with a random password and log the credentials when Keystone starts. The creation runs in the background so it does not delay startup. This is convenient for development; production systems should use a secure provisioning process instead. ## What we have now ```ts // auth.ts import { createAuth } from '@keystone-6/auth' import { statelessSessions } from '@keystone-6/core/session' const { withAuth } = createAuth({ listKey: 'User', identityField: 'email', sessionData: 'name', secretField: 'password', }) let sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --' let sessionMaxAge = 60 * 60 * 24 // 24 hours const session = statelessSessions({ maxAge: sessionMaxAge, secret: sessionSecret, }) export { withAuth, session } ``` ```ts //keystone.ts import { list, config } from '@keystone-6/core' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { password, text, timestamp, select, relationship } from '@keystone-6/core/fields' import { withAuth, session } from './auth' const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), password: password({ validation: { isRequired: true } }), }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], defaultValue: 'draft', ui: { displayMode: 'segmented-control' }, }), author: relationship({ ref: 'User.posts' }), }, }), } export default config( withAuth({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), async onConnect(context) { // this creates an initial user if none exist so you can log in for development // WARNING: do not use this in production ;(async () => { const sudoContext = context.sudo() if ((await sudoContext.db.User.count()) !== 0) return const password = crypto.getRandomValues(new Uint8Array(16)).toHex() await sudoContext.db.User.createOne({ data: { name: 'admin', email: 'admin@example.com', password }, }) console.log(`Created initial user: admin@example.com / ${password}`) })().catch(error => console.error('Failed to create initial user:', error)) }, }, lists, session, ui: { isAccessAllowed: context => !!context.session?.data, }, }) ) ``` ## Next lesson {% related-content %} {% well heading="Lesson 5: Rich Text" grad="grad1" href="/docs/walkthroughs/lesson-5" target="" %} Add a powerful `document` field to your app and learn how to configure it to meet your needs {% /well %} {% /related-content %} --- # Lesson 5: Document field Source: https://keystonejs.com/docs/walkthroughs/lesson-5 Learn how to implement a powerful and customisable Rich Text editing experience with Keystone’s `document` field. ## Where we left off We have a working backend for a collection of interconnected posts and authors (in the User list), and thanks to our efforts in [lesson 4](/docs/walkthroughs/lesson-4) our app is now secured so that only registered users can access the Admin UI to write and edit posts. ```ts //keystone.ts import { list, config } from '@keystone-6/core' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { password, text, timestamp, select, relationship } from '@keystone-6/core/fields' import { withAuth, session } from './auth' const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), password: password({ validation: { isRequired: true } }), }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], defaultValue: 'draft', ui: { displayMode: 'segmented-control' }, }), author: relationship({ ref: 'User.posts' }), }, }), } export default config( withAuth({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, session, ui: { isAccessAllowed: context => !!context.session?.data, }, }) ) ``` Back in [Lesson 2](/docs/walkthroughs/lesson-4) we setup a `post` type, but we skipped the all important "content" part. Let’s go ahead and fill that gap. ## Add the Document field Keystone’s [document](https://keystonejs.com/docs/fields/document) field is a highly customisable Rich Text editor that lets content creators quickly and easily edit content in your system. To implement the document field we start by adding the package to our project: ```sh npm install @keystone-6/fields-document ``` Next, we add the document field to our `post` list: ```ts{5,29}[9-15,32-500] // keystone.ts import { list, config } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { password, text, timestamp, select, relationship } from '@keystone-6/core/fields'; import { document } from '@keystone-6/fields-document'; import { withAuth, session } from './auth'; const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), password: password({ validation: { isRequired: true } }) }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], defaultValue: 'draft', }), author: relationship({ ref: 'User.posts' }), content: document() }, }), }; export default config( withAuth({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, session, ui: { isAccessAllowed: (context) => !!context.session?.data }, }) ); ``` Let’s startup Keystone to see the document field in a post: ![Create user screen in Admin UI showing name, email, and password fields](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-5/document-field-basic.png) We have a place to write Rich Text, but the control header needs configuring to make this a good editor experience. ## Customise the Document field Let’s start by adding four [formatting](/docs/guides/document-fields#formatting) options available in the document field’s config: - formatting (bold, underline, italics) - links - dividers - layouts ```ts{29-40}[1-7,9-15,18-28,43-500] // keystone.ts import { list, config } from '@keystone-6/core'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; import { password, text, timestamp, select, relationship } from '@keystone-6/core/fields'; import { document } from '@keystone-6/fields-document'; import { withAuth, session } from './auth'; const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), password: password({ validation: { isRequired: true } }) }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], defaultValue: 'draft', }), author: relationship({ ref: 'User.posts' }), content: document({ formatting: true, links: true, dividers: true, layouts: [ [1, 1], [1, 1, 1], [2, 1], [1, 2], [1, 2, 1], ], }), }, }), }; export default config( withAuth({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, session, ui: { isAccessAllowed: (context) => !!context.session?.data }, }) ); ``` And take a look at how they can be used in Admin UI: ![Screenshot of the admin UI with the content field visible](https://keystonejs.s3.amazonaws.com/framework-assets/assets/walkthroughs/lesson-5/document-field.png) For a full guide on using the document field, including rendering this in your own frontend app, check out the full guide on [how to use document fields](/docs/guides/document-fields) ## What we have now ```ts // keystone.ts import { list, config } from '@keystone-6/core' import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { password, text, timestamp, select, relationship } from '@keystone-6/core/fields' import { document } from '@keystone-6/fields-document' import { withAuth, session } from './auth' const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique' }), posts: relationship({ ref: 'Post.author', many: true }), password: password({ validation: { isRequired: true } }), }, }), Post: list({ fields: { title: text(), publishedAt: timestamp(), status: select({ options: [ { label: 'Published', value: 'published' }, { label: 'Draft', value: 'draft' }, ], defaultValue: 'draft', }), author: relationship({ ref: 'User.posts' }), content: document({ formatting: true, links: true, dividers: true, layouts: [ [1, 1], [1, 1, 1], [2, 1], [1, 2], [1, 2, 1], ], }), }, }), } export default config( withAuth({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: 'file:./keystone.db' }), }), }, lists, session, ui: { isAccessAllowed: context => !!context.session?.data }, }) ) ``` ## Next steps Congratulations! You've now built a Keystone app from an empty folder and have the basics in place for a functional blog that you can could connect to a frontend of your choice. This lesson marks the end of this learning series. To dive deeper into Keystone's capabilities take a look at the following: {% related-content %} {% well heading="Examples" grad="grad1" href="/docs/examples" target="" %} A growing collection of projects you can run locally to learn more about Keystone’s capabilities {% /well %} {% well heading="Guides" grad="grad1" href="/docs/guides/overview" target="" %} Practical explanations of Keystone's fundamental building blocks {% /well %} {% /related-content %}