搜索

PythonReactSEOPDF
GitHub Skill

keystonejs

Builds content APIs with KeystoneJS, the Node.js headless CMS with GraphQL. Use when creating admin UIs and APIs with automatic CRUD operations, relationships, access control, and TypeScript support.

mgd34msu☆ 3 仓库 Stars0 仓库 Forks

安装与使用

请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。

查看来源 ↗使用指南

技能说明

KeystoneJS

Open-source Node.js headless CMS that auto-generates GraphQL API and Admin UI from your schema. TypeScript-first with powerful access control.

Quick Start

npm create keystone-app@latest my-app
cd my-app
npm run dev

Opens:

  • Admin UI: http://localhost:3000
  • GraphQL Playground: http://localhost:3000/api/graphql

Configuration

// keystone.ts
import { config } from '@keystone-6/core';
import { lists } from './schema';
import { withAuth, session } from './auth';

export default withAuth(
  config({
    db: {
      provider: 'postgresql',  // or 'sqlite', 'mysql'
      url: process.env.DATABASE_URL!,
    },
    lists,
    session,
    ui: {
      isAccessAllowed: (context) => !!context.session?.data,
    },
  })
);

Schema Definition

// schema.ts
import { list } from '@keystone-6/core';
import { text, timestamp, relationship, select, checkbox, password } from '@keystone-6/core/fields';
import { document } from '@keystone-6/fields-document';

export 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({ validation: { isRequired: true } }),
      slug: text({ isIndexed: 'unique' }),
      status: select({
        options: [
          { label: 'Draft', value: 'draft' },
          { label: 'Published', value: 'published' },
        ],
        defaultValue: 'draft',
        ui: { displayMode: 'segmented-control' },
      }),
      content: document({
        formatting: true,
        links: true,
        dividers: true,
        layouts: [
          [1, 1],
          [1, 1, 1],
        ],
      }),
      publishedAt: timestamp(),
      author: relationship({
        ref: 'User.posts',
        ui: {
          displayMode: 'cards',
          cardFields: ['name', 'email'],
          inlineCreate: { fields: ['name', 'email'] },
        },
      }),
      tags: relationship({
        ref: 'Tag.posts',
        many: true,
        ui: {
          displayMode: 'select',
          labelField: 'name',
        },
      }),
    },
    hooks: {
      resolveInput: async ({ resolvedData, inputData }) => {
        // Auto-generate slug from title
        if (inputData.title && !inputData.slug) {
          resolvedData.slug = inputData.title.toLowerCase().replace(/\s+/g, '-');
        }
        return resolvedData;
      },
    },
  }),

  Tag: list({
    fields: {
      name: text({ validation: { isRequired: true } }),
      posts: relationship({ ref: 'Post.tags', many: true }),
    },
  }),
};

Field Types

import {
  text,
  password,
  integer,
  float,
  decimal,
  checkbox,
  select,
  multiselect,
  timestamp,
  calendarDay,
  json,
  relationship,
  file,
  image,
} from '@keystone-6/core/fields';
import { document } from '@keystone-6/fields-document';

// Text
text({ validation: { isRequired: true, length: { max: 255 } } })

// Password (hashed)
password({ validation: { isRequired: true } })

// Numbers
integer({ defaultValue: 0 })
float()
decimal({ precision: 10, scale: 2 })

// Boolean
checkbox({ defaultValue: false })

// Select
select({
  options: [
    { label: 'Draft', value: 'draft' },
    { label: 'Published', value: 'published' },
  ],
  defaultValue: 'draft',
})

// Multi-select
multiselect({
  options: [
    { label: 'React', value: 'react' },
    { label: 'Vue', value: 'vue' },
    { label: 'Svelte', value: 'svelte' },
  ],
})

// Dates
timestamp()
calendarDay()

// JSON
json()

// Relationship
relationship({
  ref: 'Post.author',  // Related list.field
  many: false,         // One or many
})

// File upload
file({ storage: 's3' })

// Image with transforms
image({ storage: 'local' })

// Rich text (Document)
document({
  formatting: true,
  links: true,
  dividers: true,
  layouts: [[1, 1]],
})

Access Control

import { list } from '@keystone-6/core';

export const lists = {
  Post: list({
    access: {
      operation: {
        query: () => true,  // Anyone can query
        create: ({ session }) => !!session,  // Logged in only
        update: ({ session }) => !!session,
        delete: ({ session }) => session?.data.isAdmin,  // Admin only
      },

      filter: {
        query: ({ session }) => {
          // Non-logged in users only see published
          if (!session) {
            return { status: { equals: 'published' } };
          }
          return true;  // Logged in see all
        },
      },

      item: {
        update: ({ session, item }) => {
          // Users can only update their own posts
          if (session?.data.isAdmin) return true;
          return session?.itemId === item.authorId;
        },
      },
    },
    fields: {/* ... */},
  }),
};

Hooks

export const lists = {
  Post: list({
    hooks: {
      // Before validation
      resolveInput: async ({ resolvedData, inputData, item, context }) => {
        // Modify data before saving
        if (inputData.title) {
          resolvedData.slug = inputData.title.toLowerCase().replace(/\s+/g, '-');
        }
        return resolvedData;
      },

      // Validation
      validateInput: async ({ resolvedData, addValidationError }) => {
        if (resolvedData.title?.length < 3) {
          addValidationError('Title must be at least 3 characters');
        }
      },

      // Before save
      beforeOperation: async ({ operation, resolvedData, context }) => {
        if (operation === 'create') {
          // Set author to current user
          resolvedData.author = { connect: { id: context.session?.itemId } };
        }
      },

      // After save
      afterOperation: async ({ operation, item, context }) => {
        if (operation === 'create') {
          console.log(`New post created: ${item.title}`);
          // Send notification, revalidate cache, etc.
        }
      },
    },
    fields: {/* ... */},
  }),
};

GraphQL API

Auto-generated based on your schema.

Queries
# Get all posts
query {
  posts {
    id
    title
    status
    author {
      name
    }
  }
}

# Get single post
query {
  post(where: { id: "123" }) {
    title
    content {
      document
    }
  }
}

# Filter and sort
query {
  posts(
    where: {
      status: { equals: "published" }
      title: { contains: "javascript" }
    }
    orderBy: { publishedAt: desc }
    take: 10
    skip: 0
  ) {
    id
    title
    publishedAt
  }
}

# Count
query {
  postsCount(where: { status: { equals: "published" } })
}
Mutations
# Create
mutation {
  createPost(data: {
    title: "New Post"
    content: { document: [...] }
    author: { connect: { id: "user-id" } }
  }) {
    id
    title
  }
}

# Update
mutation {
  updatePost(
    where: { id: "123" }
    data: { title: "Updated Title" }
  ) {
    id
    title
  }
}

# Delete
mutation {
  deletePost(where: { id: "123" }) {
    id
  }
}

# Create many
mutation {
  createPosts(data: [
    { title: "Post 1" },
    { title: "Post 2" }
  ]) {
    id
    title
  }
}
Filter Operators
where: {
  # Equality
  title: { equals: "Hello" }
  title: { not: { equals: "Hello" } }

  # String matching
  title: { contains: "react" }
  title: { startsWith: "How to" }
  title: { endsWith: "Guide" }

  # Comparison
  views: { gt: 100 }
  views: { gte: 100 }
  views: { lt: 1000 }
  views: { lte: 1000 }

  # List
  status: { in: ["draft", "review"] }
  status: { notIn: ["archived"] }

  # Relationship
  author: { id: { equals: "user-id" } }
  author: { name: { contains: "John" } }
  tags: { some: { name: { equals: "react" } } }
  tags: { every: { name: { in: ["react", "javascript"] } } }
  tags: { none: { name: { equals: "deprecated" } } }

  # Logical
  AND: [{ status: { equals: "published" } }, { featured: { equals: true } }]
  OR: [{ status: { equals: "published" } }, { author: { id: { equals: "me" } } }]
  NOT: { status: { equals: "archived" } }
}

Query API (Server-Side)

// In resolvers, hooks, or custom API routes
const posts = await context.query.Post.findMany({
  where: { status: { equals: 'published' } },
  orderBy: { publishedAt: 'desc' },
  take: 10,
  query: 'id title slug author { name }',
});

const post = await context.query.Post.findOne({
  where: { id: 'post-id' },
  query: 'id title content { document } author { name email }',
});

// Create
const newPost = await context.query.Post.createOne({
  data: {
    title: 'New Post',
    author: { connect: { id: context.session?.itemId } },
  },
  query: 'id title',
});

// Update
await context.query.Post.updateOne({
  where: { id: 'post-id' },
  data: { title: 'Updated' },
});

// Delete
await context.query.Post.deleteOne({
  where: { id: 'post-id' },
});

Authentication

// auth.ts
import { createAuth } from '@keystone-6/auth';
import { statelessSessions } from '@keystone-6/core/session';

const { withAuth } = createAuth({
  listKey: 'User',
  identityField: 'email',
  secretField: 'password',
  sessionData: 'id name email isAdmin',
  initFirstItem: {
    fields: ['name', 'email', 'password'],
  },
});

const session = statelessSessions({
  maxAge: 60 * 60 * 24 * 30,  // 30 days
  secret: process.env.SESSION_SECRET!,
});

export { withAuth, session };

File Storage

// keystone.ts
import { config } from '@keystone-6/core';

export default config({
  storage: {
    local: {
      kind: 'local',
      type: 'file',
      generateUrl: (path) => `/files${path}`,
      serverRoute: { path: '/files' },
      storagePath: 'public/files',
    },
    s3: {
      kind: 's3',
      type: 'file',
      bucketName: process.env.S3_BUCKET!,
      region: process.env.S3_REGION!,
      accessKeyId: process.env.S3_ACCESS_KEY!,
      secretAccessKey: process.env.S3_SECRET_KEY!,
    },
  },
  // ...
});

Custom GraphQL

import { graphql } from '@keystone-6/core';

export const lists = {
  Post: list({
    fields: {/* ... */},
  }),
};

export const extendGraphqlSchema = graphql.extend((base) => ({
  query: {
    featuredPosts: graphql.field({
      type: graphql.list(graphql.nonNull(base.object('Post'))),
      resolve: async (root, args, context) => {
        return context.query.Post.findMany({
          where: { featured: { equals: true } },
          orderBy: { publishedAt: 'desc' },
          take: 5,
        });
      },
    }),
  },
  mutation: {
    publishPost: graphql.field({
      type: base.object('Post'),
      args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) },
      resolve: async (root, { id }, context) => {
        return context.query.Post.updateOne({
          where: { id },
          data: {
            status: 'published',
            publishedAt: new Date().toISOString(),
          },
        });
      },
    }),
  },
}));

Next.js Integration

// lib/keystone.ts
const KEYSTONE_URL = process.env.KEYSTONE_URL || 'http://localhost:3000';

export async function fetchGraphQL(query: string, variables = {}) {
  const response = await fetch(`${KEYSTONE_URL}/api/graphql`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });

  const { data, errors } = await response.json();

  if (errors) {
    throw new Error(errors[0].message);
  }

  return data;
}

// Typed fetchers
export async function getPosts() {
  const { posts } = await fetchGraphQL(`
    query {
      posts(where: { status: { equals: "published" } }, orderBy: { publishedAt: desc }) {
        id
        title
        slug
        excerpt
        publishedAt
        author { name }
      }
    }
  `);
  return posts;
}

export async function getPost(slug: string) {
  const { posts } = await fetchGraphQL(`
    query GetPost($slug: String!) {
      posts(where: { slug: { equals: $slug } }) {
        id
        title
        content { document }
        author { name }
      }
    }
  `, { slug });
  return posts[0];
}

Deployment

# Build
npm run build

# Start production
npm run start

Database migrations are handled automatically. For production:

  1. Use PostgreSQL or MySQL (not SQLite)
  2. Set SESSION_SECRET environment variable
  3. Configure storage for uploads (S3 recommended)

Best Practices

  1. Use TypeScript for full type safety
  2. Define access control for all lists
  3. Use hooks for business logic and side effects
  4. Extend GraphQL for custom operations
  5. Set up proper relationships with ref bidirectional references
  6. Use the Query API for server-side data fetching

使用示例

查看来源仓库中的使用示例与配置说明。

查看来源 ↗

来源历史

收录时间
2026-01-20
索引更新时间
2026-01-28
Branch
main

收录与同步日期属于本站记录。源码修改历史请查看来源仓库。

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!