Documentation
Query the database
GraphQL

Query the GraphQL API

This guide assumes you have a working knowledge of GraphQL query syntax. For a general overview of GraphQL, please refer to the documentation.

Ponder uses your ponder.schema.ts file to generate a GraphQL API for your app. With the dev server running, open http://localhost:42069 in your browser to use the GraphiQL interface. GraphiQL is a useful tool for exploring your schema and testing queries during development.

Schema generation

Ponder creates a singular and a plural query field for each table in your schema. For example, if your schema contains a Person table, Ponder will create a person and a persons field on the root Query type. The singular query field returns a single record (or null), while the plural query field returns a page of records.

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Person: p.createTable({
    id: p.int(),
    name: p.string(),
    age: p.int().optional(),
  }),
}));
Generated schema
type Person {
  id: Int!
  name: String!
  age: Int
}
 
type PersonPage {
  items: [Person!]!
  pageInfo: PageInfo!
}
 
type Query {
  person(id: Int!, timestamp: Int): Person
  persons(
    where: PetFilter, 
    orderBy: String,
    orderDirection: String,
    before: String,
    after: String,
    limit: Int,
    timestamp: Int,
  ): PersonPage!
}

Filtering

Use the where argument to filter for records that match the specified criteria.

The where argument type includes filter options for every column defined on a table. Here are the filter options available for each column type:

Filter optionAvailable for column typesInclude records where {column}...
{column}Allequals the value
{column}_notAlldoes not equal the value
{column}_inAll primitives and enumsis one of the values
{column}_not_inAll primitives and enumsis not one of the values
{column}_gtNumeric primitives (int, float, bigint, hex)is greater than the value
{column}_ltNumeric primitivesis less than the value
{column}_gteNumeric primitivesis greater than or equal to the value
{column}_lteNumeric primitivesis less than or equal to the value
{column}_containsString primitives (string)contains the substring
{column}_not_containsString primitivesdoes not contain the substring
{column}_starts_withString primitivesstarts with the substring
{column}_not_starts_withString primitivesdoes not start with the substring
{column}_ends_withString primitivesends with the substring
{column}_not_ends_withString primitivesdoes not end with the substring
{column}_hasLists of primitives and enumshas the value as an element
{column}_not_hasLists of primitives and enumsdoes not have the value as an element

You can also compose filters using the AND and OR operators. These special fields accept an array of filter objects.

Examples

For the following examples, assume these records exist in your database.

Person data
[
  { "id": 1, "name": "Barry", "age": 57 },
  { "id": 2, "name": "Lucile", "age": 32 },
  { "id": 3, "name": "Sally", "age": 22 },
  { "id": 4, "name": "Pablo", "age": 71 },
]

Get all Person records with an age greater than 32.

Query
query {
  persons(where: { age_gt: 32 }) {
    name
    age
  }
}
Result
{
  "persons": [
    { "name": "Barry", "age": 57 },
    { "name": "Pablo", "age": 71 },
  ]
}

Get all Person records with a name that does not end with "y" and an age greater than 60. Note that when you include multiple filter conditions, they are combined with a logical AND.

Query
query {
  persons(where: { name_not_ends_with: "y" }) {
    name
    age
  }
}
Result
{
  "persons": [
    { "name": "Lucile", "age": 32 },
    { "name": "Pablo", "age": 71 },
  ]
}

Get all Person records with a name that contains "ll" or an age greater than or equal to 60. In this case, we use the special OR operator to combine multiple filter conditions.

Query
query {
  persons(
    where: {
      OR: [
        { name_contains: "ll" },
        { age_gte: 60 }
      ] 
    }
  ) {
    name
    age
  }
}
Result
{
  "persons": [
    { "name": "Barry", "age": 57 },
    { "name": "Sally", "age": 22 },
    { "name": "Pablo", "age": 71 },
  ]
}

Sorting

Use the orderBy and orderDirection arguments to sort records by a column. string values are sorted lexicographically.

Pagination optionDefault
orderBy"id"
orderDirection"asc"

Examples

Query
query {
  persons(orderBy: "age", orderDirection: "desc") {
    name
    age
  }
}
Result
{
  "persons": [
    { "name": "Pablo", "age": 71 },
    { "name": "Barry", "age": 57 },
    { "name": "Lucile", "age": 32 },
    { "name": "Sally", "age": 22 },
  ]
}

Pagination

The GraphQL API supports cursor pagination using an API that's inspired by the Relay GraphQL Cursor Connection specification.

  • Cursor values are opaque strings that encode the position of a record in the result set. They should not be decoded or manipulated by the client.
  • Cursors are exclusive, meaning that the record at the specified cursor is not included in the result.
  • Cursor pagination works with any valid filter and sort criteria. However, do not change the filter or sort criteria between paginated requests. This will cause validation errors or incorrect results.

Page type

Top-level plural query fields and p.many() relationship fields return a Page type containing a list of items and a PageInfo object.

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Pet: p.createTable({
    id: p.string(),
    name: p.string(),
  }),
}));
Generated schema
type PageInfo {
  startCursor: String
  endCursor: String
  hasPreviousPage: Boolean!
  hasNextPage: Boolean!
}
 
type Pet {
  id: String!
  name: String!
}
 
type PetPage {
  items: [Pet!]!
  pageInfo: PageInfo!
}

Here is a detailed view of the PageInfo object:

nametype
startCursorStringCursor of the first record in items
endCursorStringCursor of the last record in items
hasPreviousPageBoolean!Whether there are more records before this page
hasNextPageBoolean!Whether there are more records after this page

Examples

As a reminder, assume that these records exist in your database for the following examples.

Person data
[
  { "id": 1, "name": "Barry", "age": 57 },
  { "id": 2, "name": "Lucile", "age": 32 },
  { "id": 3, "name": "Sally", "age": 22 },
  { "id": 4, "name": "Pablo", "age": 71 },
]

First, make a request without specifying any pagination options. The items list will contain the first n=limit records that match the filter and sort criteria.

Query one
query {
  persons(orderBy: "age", orderDirection: "asc", limit: 2) {
    items {
      name
      age
    }
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
Result one
{
  "persons" {
    "items": [
      { "name": "Sally", "age": 22 },
      { "name": "Lucile", "age": 32 },
    ],
    "pageInfo": {
      "startCursor": "MfgBzeDkjs44",
      "endCursor": "Mxhc3NDb3JlLTA=",
      "hasPreviousPage": false,
      "hasNextPage": true,
    }
  }
}

To paginate forwards, pass pageInfo.endCursor from the previous request as the after option in the next request.

Query one
query {
  persons(
    orderBy: "age",
    orderDirection: "asc",
    limit: 2,
    after: "Mxhc3NDb3JlLTA="
  ) {
    items {
      name
      age
    }
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
Result one
{
  "persons" {
    "items": [
      { "name": "Barry", "age": 57 },
      { "name": "Pablo", "age": 71 },
    ],
    "pageInfo": {
      "startCursor": "MxhcdoP9CVBhY",
      "endCursor": "McSDfVIiLka==",
      "hasPreviousPage": true,
      "hasNextPage": false,
    }
  }
}

To paginate backwards, pass pageInfo.startCursor from the previous request as the before option in the next request.

Query three
query {
  persons(
    orderBy: "age",
    orderDirection: "asc",
    limit: 2,
    before: "MxhcdoP9CVBhY"
  ) {
    items {
      name
      age
    }
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}
Result three
{
  "persons" {
    "items": [
      { "name": "Lucile", "age": 32 },
    ],
    "pageInfo": {
      "startCursor": "Mxhc3NDb3JlLTA=",
      "endCursor": "Mxhc3NDb3JlLTA=",
      "hasPreviousPage": true,
      "hasNextPage": true,
    }
  }
}

Relationship fields

See the design your schema guide for a detailed overview of how to define relationships in your schema.

When you define a column in your schema using p.one() or p.many(), Ponder creates a one-to-one or one-to-many relationship field in the GraphQL schema.

Fields created by p.many() are very similar to the top-level plural query field, except they are automatically filtered by the parent entity ID.

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Pet: p.createTable({
    id: p.string(),
    name: p.string(),
    ownerId: p.int().references("Person.id"),
    owner: p.one("ownerId"),
  }),
  Person: p.createTable({
    id: p.int(),
    dogs: p.many("Pet.ownerId"),
  }),
}));
Generated schema
type Pet {
  id: String!
  name: String!
  ownerId: Int!
  owner: Person!
}
 
type Person {
  id: Int!
  pets(
    # Has { ownerId: person.id } applied
    where: PetFilter, 
    orderBy: String,
    orderDirection: String,
    before: String,
    after: String,
    limit: Int,
    timestamp: Int,
  ): PetPage!
}

How to speed up queries

Here are a few tips for speeding up slow queries.

  1. Create database indexes: Create indexes to speed up filters, joins, and sort conditions. Read more.
  2. Enable horizontal scaling: If the GraphQL API is struggling to keep up with reqeust volume, consider spreading the load across multiple instances. Read more.
  3. Limit query depth: Each layer of depth in a GraphQL query introduces at least one additional sequential database query. Avoid queries that are more than 2 layers deep.
  4. Use pagination: Use cursor-based pagination to fetch records in smaller, more manageable chunks. This can help reduce the load on the database.

Time-travel queries

⚠️
Native support for time-travel queries was removed in 0.4.0. Read more.