Keystone Headless CMS

website·Indexed 35 days ago

https://keystonejs.com/

A programmable open-source GraphQL API back-end and headless CMS for Node.js. It features a customizable Admin UI, a powerful system configuration for defining data models via lists and fields, and built-in support for authentication, access control, and database migrations using Prisma. Supports PostgreSQL, MySQL, and SQLite.

Tokens
45.2K
Snippets
201
Records
266
Agent score
69%

What's inside Keystone

  1. Overview of Keystone CLI commands

    The keystone CLI is used to develop, build, and deploy Keystone projects. All commands require a keystone.js or keystone.ts file with a default export returning a Keystone System config from @keystone-6/core.
    $ keystone [command]
    
    Commands:
      dev           Start the project in development mode (default)
      postinstall   Build the project for development (optional)
      build         Build the project (required for 'keystone start')
      start         Start the project in production mode
      telemetry     Set telemetry preference (enable/disable/status)
  2. Overview of KeystoneJS Access Control

    KeystoneJS provides two levels of access control:

    1. List Access Control: Configured via the access property of the list configuration. It determines who can query, create, update, and delete items in a list.
    2. Field Access Control: Configured via the access property of a field configuration. It determines who can read, create, and update specific field values.

    Access control is applied before hooks. Note that Keystone does not automatically leverage database row-level security (RLS) policies.

    import { config, list } from '@keystone-6/core';
    import { text } from '@keystone-6/core/fields';
    
    export default config({
      lists: {
        ListKey: list({
          fields: {
            fieldName: text({ access: { /* field level access */ }, }),
          },
          access: { /* list level access */ },
        }),
      },
    });
  3. Overview of KeystoneJS Mutation Hooks

    Hooks allow the execution of custom code at various stages of the mutation lifecycle during create, update, and delete operations. Hooks can be defined at both the List level and the Field level.

    Execution Order: For any given operation, field hooks are applied to all fields first in parallel, followed by the list hooks.

    General Characteristics:

    • Most hooks support async functions.
    • Except for resolveInput, hooks do not require a return value.
    • When operating on multiple items, hooks are called individually for each item being processed.
    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 => { /* ... */ } },
                // ... other hooks follow same pattern as list hooks
              },
            }),
          },
        }),
      },
    });
  4. Overview of KeystoneJS Hooks

    Hooks are functions defined in the schema configuration that execute during GraphQL operations (create, update, delete). They allow developers to inject business logic into the CRUD lifecycle. Hooks can be defined at the List level or the Field level. All hooks have access to a context argument providing the full context API.
    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: ${item.name}`);
              }
            }
          },
        }),
      },
    });
  5. Use the KeystoneJS Query API for programmatic CRUD operations

    The Query API allows you to execute CRUD operations against your GraphQL API programmatically. For every list defined in your system, an API is available at context.query.<listName>. These functions execute queries and mutations directly against the GraphQL API, and their arguments closely mirror the GraphQL API inputs.

    A key parameter across these methods is query (which defaults to 'id'), a string specifying which fields should be returned by the operation.

    // Available methods on context.query.<listName>
    {
      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 }),
    }
  6. Use the KeystoneJS Database API for programmatic CRUD

    The Database API (context.db) allows you to run CRUD operations against internal GraphQL resolvers. Unlike the GraphQL Server, it invokes resolver functions directly and returns internal item objects.

    Key characteristics:

    • Security: It executes defined access control rules and hooks.
    • Bypassing: To bypass access control and hooks, use the Prisma Client via context.prisma instead.
    • Availability: For every list defined in the system, the API is available at context.db.<listName>.
  7. Enable OpenTelemetry tracing

    Keystone has built-in support for OpenTelemetry tracing via @opentelemetry/api. It automatically emits trace spans for internal operations like CRUD resolvers and hooks. No Keystone-specific configuration is needed; simply initialize an OpenTelemetry SDK in your application before Keystone starts.
  8. Use the BigInt field in KeystoneJS

    The bigInt field represents a 64-bit signed integer. Because JavaScript numbers are unsafe beyond 2^53, this field handles values as follows:

    • GraphQL: Values are transmitted as strings.
    • Hooks: Values are handled as native JavaScript BigInt types.
  9. Understand Keystone's generated GraphQL CRUD API

    Keystone automatically generates a CRUD GraphQL API based on the lists defined in your system configuration. The names of the queries, mutations, and types are derived directly from the names of the lists and fields in your config.

    For a list named User, Keystone generates:

    • Queries:
      • user: Fetch a single record using a unique identifier.
      • users: Fetch a list of records with support for filtering (where), sorting (orderBy), and pagination (take, skip).
      • usersCount: Return the total number of records matching a filter.
    • Mutations:
      • createUser / createUsers: Create one or multiple records.
      • updateUser / updateUsers: Update one or multiple records.
      • deleteUser / deleteUsers: Delete one or multiple records.
    // System config that triggers the generation of the User API
    import { config, list } from '@keystone-6/core';
    import { text } from '@keystone-6/core/fields';
    
    export default config({
      lists: {
        User: list({ fields: { name: text() } }),
      },
    });
  10. Use the Keystone Context object for business logic

    The Context object is provided to every resolver in a Keystone system. It is the primary entry point for implementing business logic in access control, hooks, testing, and GraphQL schema extensions. It provides access to the Query API, Database API, HTTP request/response objects, session data, and GraphQL execution helpers.
    import type { Context } from './generated/keystone/types'
    
    // Example of the Context object structure
    const context = {
      query,           // Query API
      db,             // Database API
      req,            // IncomingMessage
      res,            // ServerResponse
      session,        // Current session data
      sessionStrategy,// Session manipulation functions
      graphql: {
        schema,        // GraphQL Schema object
        run,           // Executes query and returns data
        raw,          // Executes query and returns { data, errors }
      },
      sudo,           // Bypasses access control
      internal,       // Bypasses graphql.omit
      withRequest,     // Creates context with specific req/res
      withSession,    // Creates context with specific session
      transaction,    // Runs a function in a Prisma transaction
      prisma,         // Raw Prisma Client access
    }
  11. Update custom Admin UI dependencies

    Replace all @keystone-ui/* imports with @keystar/ui. Note that the exports are not directly equivalent and will require manual updates to imports and usage. Additionally, remove imports from @keystone-6/fields-document/primitives as they are no longer exported.
  12. Implement User Authentication and Sessions in Keystone 6

    To enable authentication in a Keystone app, you must add a password field to the User list and wrap the Keystone configuration with withAuth. This setup allows for session management and access control for the Admin UI.
    // keystone.ts
    import { list, config } from '@keystone-6/core'
    import { password, text, 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' }),
          password: password({ validation: { isRequired: true } }),
          posts: relationship({ ref: 'Post.author', many: true }),
        },
      }),
      Post: list({
        fields: {
          title: text(),
          author: relationship({ ref: 'User.posts' }),
        },
      }),
    }
    
    export default config(
      withAuth({
        db: {
          provider: 'sqlite',
          // ... db config
        },
        lists,
        session,
        ui: {
          // Restrict Admin UI access to authenticated users
          isAccessAllowed: context => !!context.session?.data,
        },
      })
    )