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.
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Person: p.createTable({
id: p.int(),
name: p.string(),
age: p.int().optional(),
}),
}));
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 option | Available for column types | Include records where {column}... |
---|---|---|
{column} | All | equals the value |
{column}_not | All | does not equal the value |
{column}_in | All primitives and enums | is one of the values |
{column}_not_in | All primitives and enums | is not one of the values |
{column}_gt | Numeric primitives (int , float , bigint , hex ) | is greater than the value |
{column}_lt | Numeric primitives | is less than the value |
{column}_gte | Numeric primitives | is greater than or equal to the value |
{column}_lte | Numeric primitives | is less than or equal to the value |
{column}_contains | String primitives (string ) | contains the substring |
{column}_not_contains | String primitives | does not contain the substring |
{column}_starts_with | String primitives | starts with the substring |
{column}_not_starts_with | String primitives | does not start with the substring |
{column}_ends_with | String primitives | ends with the substring |
{column}_not_ends_with | String primitives | does not end with the substring |
{column}_has | Lists of primitives and enums | has the value as an element |
{column}_not_has | Lists of primitives and enums | does 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.
[
{ "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 {
persons(where: { age_gt: 32 }) {
name
age
}
}
{
"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 {
persons(where: { name_not_ends_with: "y" }) {
name
age
}
}
{
"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 {
persons(
where: {
OR: [
{ name_contains: "ll" },
{ age_gte: 60 }
]
}
) {
name
age
}
}
{
"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 option | Default |
---|---|
orderBy | "id" |
orderDirection | "asc" |
Examples
query {
persons(orderBy: "age", orderDirection: "desc") {
name
age
}
}
{
"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.
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Pet: p.createTable({
id: p.string(),
name: p.string(),
}),
}));
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:
name | type | |
---|---|---|
startCursor | String | Cursor of the first record in items |
endCursor | String | Cursor of the last record in items |
hasPreviousPage | Boolean! | Whether there are more records before this page |
hasNextPage | Boolean! | Whether there are more records after this page |
Examples
As a reminder, assume that these records exist in your database for the following examples.
[
{ "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 {
persons(orderBy: "age", orderDirection: "asc", limit: 2) {
items {
name
age
}
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
}
}
{
"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 {
persons(
orderBy: "age",
orderDirection: "asc",
limit: 2,
after: "Mxhc3NDb3JlLTA="
) {
items {
name
age
}
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
}
}
{
"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 {
persons(
orderBy: "age",
orderDirection: "asc",
limit: 2,
before: "MxhcdoP9CVBhY"
) {
items {
name
age
}
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
}
}
{
"persons" {
"items": [
{ "name": "Lucile", "age": 32 },
],
"pageInfo": {
"startCursor": "Mxhc3NDb3JlLTA=",
"endCursor": "Mxhc3NDb3JlLTA=",
"hasPreviousPage": true,
"hasNextPage": true,
}
}
}
Relationship fields
See the Define 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.
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"),
}),
}));
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!
}
Time-travel queries
Using time-travel queries, you can query the state of the database at any point in history. To construct a time-travel query, pass a Unix timestamp to the timestamp
argument on any of the root query types.
Time-travel option | Default |
---|---|
timestamp | undefined ("latest") |
In this example, consider that only Pablo had been added to the database before the specified timestamp, and his age at that time was 42. The other records were inserted later.
query {
persons(timestamp: 1689910567) {
items {
name
age
}
}
}
{
"persons": {
"items": [
{ "name": "Pablo", "age": 42 },
]
}
}
timestamp
argument is inclusive. If a log was included in a block with timestamp 100, the result of a query with timestamp: 100
includes any database operations made while indexing that log.