KeystoneJS

repository·main·Indexed 11 days ago

https://github.com/keystonejs/keystone

A CMS and App Framework for developers that allows you to describe a data schema to automatically generate a GraphQL API and a Management UI for content and data management.

Tokens
115.7K
Snippets
337
Records
451
Agent score
92%

What's inside Keystone

  1. Core features of Keystone 6

    main

    Keystone 6 is a TypeScript-first Headless CMS built on modern web technologies. Key architectural components include:

    • Database Layer: Powered by Prisma, enabling automatic database migrations when you update your schema.
    • Admin UI: Powered by NextJS, providing a fast and accessible interface for managing content.
    • Data Modeling: Uses a strongly-typed developer experience with TypeScript.
    • Rich Text: Features a Document field that stores JSON-structured data, allowing for relationships, custom React-based component blocks, and slash / commands.
    • Extensibility: Supports custom Admin UI pages, logos, and navigation, as well as a Schema extension API and Virtual Fields.
  2. What is Keystone Embedded mode?

    main

    Keystone 6 supports two primary operational modes:

    1. Standalone mode: The default mode where the Content API (Keystone) is hosted separately from your frontend applications. This is ideal for scaling and multi-frontend architectures.
    2. Embedded mode: A mode where Keystone is integrated inside another application (like a Next.js app). This allows you to manage your frontend code and Keystone configuration in a single repository, simplifying development and deployment for smaller projects.

    Embedded mode often utilizes SQLite to store database content and files directly within your local repository, making it easy to commit your data state alongside your code.

  3. What is the Context object?

    main

    The Context object is the primary API entry point for all run-time functionality in a Keystone system. It is provided to every GraphQL resolver and holds essential information such as the currently logged-in user (session), access to the database, and the HTTP request/response objects.

    You use the Context object to implement:

    • Access control logic
    • Hooks
    • Testing
    • GraphQL schema extensions
    import type { Context } from './generated/keystone/types'
    
    const context: Context = {
      query,
      db,
      req,
      res,
      session,
      sessionStrategy,
      graphql: {
        schema,
        run,
        raw,
      },
      sudo,
      internal,
      withRequest,
      withSession,
      transaction,
      prisma,
    }
  4. How hooks work in Keystone

    main

    Hooks allow you to execute code at different stages of the mutation lifecycle during create, update, and delete operations.

    Key Concepts

    • Scope: Hooks can be applied to both Lists and individual Fields.
    • Execution Order: For any operation, field hooks are applied to all fields first in parallel, followed by the list hooks.
    • Async Support: Hook functions support async/await.
    • Return Values: Most hooks do not require a return value, with the exception of resolveInput, which must return the transformed data.
    • Batch Operations: 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 => { /* ... */ } },
            validate: { create: async args => { /* ... */ } },
            beforeOperation: { create: async args => { /* ... */ } },
            afterOperation: { create: async args => { /* ... */ } }
          },
          fields: {
            someFieldName: text({
              hooks: {
                resolveInput: { create: async args => { /* ... */ } },
                validate: { create: async args => { /* ... */ } },
                beforeOperation: { create: async args => { /* ... */ } },
                afterOperation: { create: async args => { /* ... */ } }
              },
            }),
          },
        }),
      },
    });
  5. Use Keystone as a Headless CMS with a frontend

    main

    Keystone functions as a Headless CMS, providing a GraphQL endpoint that any GraphQL-compatible frontend can consume.

    • Default GraphQL Endpoint: /api/graphql (e.g., http://localhost:3000/api/graphql).
    • Recommended Stack: Next.js and Apollo GraphQL.

    For practical implementations, refer to the keystone-react-todo-demo repository or the prisma-day-2021-workshop example.

  6. Build custom navigation with Keystone helper components

    main

    Keystone provides several helper components to assist in building a custom navigation bar that matches the Admin UI's look and feel:

    Wraps your navigation links in the standard Admin UI container markup.

    ListNavItems

    Automatically renders NavItem components for all provided Keystone lists. You can use the include prop to filter which lists are displayed.

    • lists: An array of ListMeta objects.
    • include (optional): An array of strings representing the key of the lists you want to show.

    A styled and accessible wrapper around Next.js Link. Use this for manual routes (like the Dashboard or external links).

    • href: The path or URL.
    • children: The label/content of the link.
    • isSelected (optional): A boolean to manually control the active/selected state. By default, it is automatically set based on the current router path.
    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 (
        <NavigationContainer>
          {/* Manual route for Dashboard */}
          <NavItem href="/">Dashboard</NavItem>
          
          {/* Automatic list rendering, optionally filtered */}
          <ListNavItems lists={lists} include={["Task"]} />
          
          {/* External or custom route */}
          <NavItem href="https://keystonejs.com/">Keystone Docs</NavItem>
        </NavigationContainer>
      )
    }
  7. Use the Keystone Query API for CRUD operations

    main

    The Query API provides a programmatic way to perform CRUD (Create, Read, Update, Delete) operations against your GraphQL API. For every list defined in your schema, these methods are available via context.query.<listName>.

    The arguments used in these functions closely mirror the GraphQL API, making it easy to transition between the two. A key argument is query (which defaults to 'id'), a string specifying which fields should be returned by the operation.

    {
      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 }),
    }
  8. Understand the Document Editor architecture

    main

    The Keystone document field is built on Slate. It stores data as a JSON blob representing the Slate document structure.

    Key architectural concepts include:

    • Data Storage: Documents are stored as JSON arrays of nodes (e.g., headings, paragraphs).
    • Plugins: Custom logic for normalization and user input is implemented via Slate plugins (functions that accept and return an Editor).
    • Normalization: Used to enforce structural rules (e.g., merging adjacent lists). Slate runs normalization on 'dirty' nodes from the deepest changed node up to the Editor.
    • Component Blocks: The primary way to customize the editor. Nodes of type component-block store form data, with children of type component-block-prop or component-inline-prop pointing to specific prop paths.
    • Relationships: Supports inline relationships or relationship component block props. These store only IDs in the database. If hydrateRelationships is enabled in GraphQL, the field populates label and data properties based on your configuration.
    [
      {
        "type": "heading",
        "level": 1,
        "children": [
          {
            "text": "content"
          }
        ]
      },
      {
        "type": "paragraph",
        "children": [
          {
            "text": "some text"
          }
        ]
      }
    ]
  9. Configure field-level access control

    main

    Keystone allows you to specify access control rules at the field level. This is useful when users have permission to access an item (list-level access) but should only be able to interact with specific fields within that item.

    You can define rules for three specific operations on a field:

    • 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.

    Important Notes:

    • To completely block users from setting a field's value, you must set both the create and update rules.
    • read field access control does not apply to context.db.* operations, as these bypass the GraphQL resolution layer.
    • Password fields never reveal their actual value; read access only determines whether the field's existence is visible.
    const Person = list({
      fields: {
        name: text(),
        email: text({
          isIndexed: 'unique',
          access: {
            read: isAdminOrPerson,
          },
        }),
        password: password({
          access: {
            read: isAdminOrPerson,
            update: isPerson,
          },
        }),
        isAdmin: checkbox({
          access: {
            read: isUser,
            update: isAdmin,
          },
        }),
      },
    })
  10. Extend Keystone with custom logic and programming

    main

    Unlike traditional configurable CMSs, Keystone is designed to be programmed. You can move beyond simple configuration by writing standard JavaScript/TypeScript code to implement complex logic.

    Key Extension Points

    • Access Control: Use TypeScript functions to define who can read, update, or create records. You can check the user's session and implement logic like "users can only update their own records" or "only admins can see email addresses."
    • Hooks: Hook into lifecycle events (e.g., resolving input) to validate or transform data before it is saved.
    • Custom Fields: Create your own field types to handle specific data requirements.
    • Custom Admin UI: Inject your own React components and views into the Admin UI.
    • Schema Extensions: Augment the GraphQL schema or underlying database queries using the Prisma client to add features like statistics or third-party microservice integration.
    • Rich Fields: Use built-in advanced fields like images or a powerful, extensible document editor that manages structured data behind a WYSIWYG interface.
  11. Understand the data resolving lifecycle for Hooks

    main

    When performing create or update operations, Keystone processes the GraphQL data input through several stages of 'data resolving' before writing to the database. You can use hooks to modify or augment the resolvedData at specific stages. The final state of resolvedData after all stages are complete is what gets validated and saved to the database.

    The stages occur in this specific order:

    1. Initialisation: resolvedData is set to the initial data input from the GraphQL mutation.
    2. Defaults (create only): Fields with default values that are undefined in resolvedData are populated with their defaults.
    3. Relationships: Relationship fields are transformed into Prisma nested write objects (e.g., { connect: [...], set: [...], disconnect: [...] }).
    4. Field values: Built-in field types convert input values into the format required for database storage.
    5. Field hooks (resolveInput): User-defined field hooks can return a new value for a specific field, which replaces the current value in resolvedData.
    6. List hooks (resolveInput): User-defined list hooks can return a new value for the entire resolvedData object.