queries.md 4.1 KB


title: Queries description: Local API, REST, and GraphQL query patterns

tags: [payload, queries, local-api, rest, graphql]

Payload CMS Queries

Query Operators

// Equals
{ color: { equals: 'blue' } }

// Not equals
{ status: { not_equals: 'draft' } }

// Greater/less than
{ price: { greater_than: 100 } }
{ age: { less_than_equal: 65 } }

// Contains (case-insensitive)
{ title: { contains: 'payload' } }

// Like (all words present)
{ description: { like: 'cms headless' } }

// In/not in
{ category: { in: ['tech', 'news'] } }

// Exists
{ image: { exists: true } }

// Near (point fields)
{ location: { near: [10, 20, 5000] } } // [lng, lat, maxDistance]

AND/OR Logic

{
  or: [
    { color: { equals: 'mint' } },
    {
      and: [
        { color: { equals: 'white' } },
        { featured: { equals: false } },
      ],
    },
  ],
}

Nested Properties

{
  'author.role': { equals: 'editor' },
  'meta.featured': { exists: true },
}

Local API

// Find documents
const posts = await payload.find({
  collection: 'posts',
  where: {
    status: { equals: 'published' },
    'author.name': { contains: 'john' },
  },
  depth: 2, // Populate relationships
  limit: 10,
  page: 1,
  sort: '-createdAt',
  locale: 'en',
  select: {
    title: true,
    author: true,
  },
})

// Find by ID
const post = await payload.findByID({
  collection: 'posts',
  id: '123',
  depth: 2,
})

// Create
const post = await payload.create({
  collection: 'posts',
  data: {
    title: 'New Post',
    status: 'draft',
  },
})

// Update
await payload.update({
  collection: 'posts',
  id: '123',
  data: {
    status: 'published',
  },
})

// Delete
await payload.delete({
  collection: 'posts',
  id: '123',
})

// Count
const count = await payload.count({
  collection: 'posts',
  where: {
    status: { equals: 'published' },
  },
})

Access Control in Local API

CRITICAL: Local API bypasses access control by default (overrideAccess: true).

// ❌ WRONG: User is passed but access control is bypassed
const posts = await payload.find({
  collection: 'posts',
  user: currentUser,
  // Result: Operation runs with ADMIN privileges
})

// ✅ CORRECT: Respects user's access control permissions
const posts = await payload.find({
  collection: 'posts',
  user: currentUser,
  overrideAccess: false, // Required to enforce access control
})

// Administrative operation (intentionally bypass access control)
const allPosts = await payload.find({
  collection: 'posts',
  // No user parameter, overrideAccess defaults to true
})

When to use overrideAccess: false:

  • Performing operations on behalf of a user
  • Testing access control logic
  • API routes that should respect user permissions

REST API

import { stringify } from 'qs-esm'

const query = {
  status: { equals: 'published' },
}

const queryString = stringify(
  {
    where: query,
    depth: 2,
    limit: 10,
  },
  { addQueryPrefix: true },
)

const response = await fetch(`https://api.example.com/api/posts${queryString}`)
const data = await response.json()

REST Endpoints

GET    /api/{collection}           - Find documents
GET    /api/{collection}/{id}      - Find by ID
POST   /api/{collection}           - Create
PATCH  /api/{collection}/{id}      - Update
DELETE /api/{collection}/{id}      - Delete
GET    /api/{collection}/count     - Count documents

GET    /api/globals/{slug}         - Get global
POST   /api/globals/{slug}         - Update global

GraphQL

query {
  Posts(where: { status: { equals: published } }, limit: 10, sort: "-createdAt") {
    docs {
      id
      title
      author {
        name
      }
    }
    totalDocs
    hasNextPage
  }
}

mutation {
  createPost(data: { title: "New Post", status: draft }) {
    id
    title
  }
}

Performance Best Practices

  • Set maxDepth on relationships to prevent over-fetching
  • Use select to limit returned fields
  • Index frequently queried fields
  • Use virtual fields for computed data
  • Cache expensive operations in hook context