Query the GraphQL API
This guide assumes you have a working knowledge of GraphQL. Visit the documentation for an overview.
Ponder automatically generates a GraphQL schema based on your ponder.schema.ts
file that uses efficient resolver logic to avoid common performance pitfalls.
GraphiQL
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
The GraphQL schema includes a singular and a plural query field for each table in ponder.schema.ts
.
If your schema contains a person
table, the GraphQL schema will include a person
and a persons
field on the root Query
type. The singular query field returns a single record (or null) and the plural query field returns a page of records.
import { onchainTable } from "@ponder/core";
export const person = onchainTable("person", (t) => ({
id: t.integer().primaryKey(),
name: t.text().notNull(),
age: t.integer(),
}));
type Query {
person(id: Int!, timestamp: Int): person
persons(
where: personFilter,
orderBy: String,
orderDirection: String,
before: String,
after: String,
limit: Int,
timestamp: Int,
): personPage!
}
type person {
id: Int!
name: String!
age: Int
}
type personPage {
items: [person!]!
pageInfo: PageInfo!
totalCount: Int!
}
Filtering
Use the where
argument to filter for records that match certain 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 | 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 | 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 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: {
AND: [
{ name_not_ends_with: "y" },
{ age_gte: 60 }
]
}
) {
name
age
}
}
{
"persons": [
{ "name": "Pablo", "age": 71 },
]
}
Get all person
records with a name
that contains "ll"
or an age greater than or equal to 50
. In this case, we use the special OR
operator to combine multiple filter conditions.
query {
persons(
where: {
OR: [
{ name_contains: "ll" },
{ age_gte: 50 }
]
}
) {
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 with an API that's inspired by the Relay GraphQL Cursor Connection specification.
Plural fields and p.many()
relationship fields each return a Page
type. This object contains a list of items, a PageInfo
object, and the total count of records that match the query.
import { onchainTable } from "@ponder/core";
export const pet = onchainTable("pet", (t) => ({
id: t.text().primaryKey(),
name: t.text().notNull(),
}));
type petPage {
items: [pet!]!
pageInfo: PageInfo!
totalCount: Int!
}
type pet {
id: String!
name: String!
}
type PageInfo {
startCursor: String
endCursor: String
hasPreviousPage: Boolean!
hasNextPage: Boolean!
}
Page info
The PageInfo
object contains information about the position of the current page within the result set.
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 |
Total count
The totalCount
field returns the number of records present in the database that match the specified query. The value the same value regardless of the current pagination position and the limit
argument. Only the where
argument can change the value of totalCount
.
The SQL query that backs totalCount
can be slow. To avoid performance
issues, include totalCount
in the query for the first page, then exclude it
for subsequent pages. Unless the underlying data has changed, the value will
be the same regardless of the pagination position.
Cursor values
A cursor value is an opaque string that encodes the position of a record in the result set.
- Cursor values should not be decoded or manipulated by the client. The only valid use of a cursor value is an argument, e.g.
after: previousPage.endCursor
. - Cursor pagination works with any filter and sort criteria. However, do not change the filter or sort criteria between paginated requests. This will cause validation errors or incorrect results.
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
}
totalCount
}
}
{
"persons" {
"items": [
{ "name": "Sally", "age": 22 },
{ "name": "Lucile", "age": 32 },
],
"pageInfo": {
"startCursor": "MfgBzeDkjs44",
"endCursor": "Mxhc3NDb3JlLTA=",
"hasPreviousPage": false,
"hasNextPage": true,
},
"totalCount": 4,
}
}
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
}
totalCount
}
}
{
"persons" {
"items": [
{ "name": "Barry", "age": 57 },
{ "name": "Pablo", "age": 71 },
],
"pageInfo": {
"startCursor": "MxhcdoP9CVBhY",
"endCursor": "McSDfVIiLka==",
"hasPreviousPage": true,
"hasNextPage": false,
},
"totalCount": 4,
}
}
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
}
totalCount
}
}
{
"persons" {
"items": [
{ "name": "Lucile", "age": 32 },
],
"pageInfo": {
"startCursor": "Mxhc3NDb3JlLTA=",
"endCursor": "Mxhc3NDb3JlLTA=",
"hasPreviousPage": true,
"hasNextPage": true,
},
"totalCount": 4,
}
}
Relationship fields
Read more about how to define relationships in your schema.
The GraphQL schema includes a relationship field for each one
or many
relation defined in your schema. One-to-many fields are very similar to the top-level plural query field, except they are automatically filtered by the parent row ID.
import { onchainTable, relations } from "@ponder/core";
export const pet = onchainTable("pet", (t) => ({
id: t.text().primaryKey(),
name: t.text().notNull(),
ownerId: t.integer().notNull(),
}));
export const petRelations = relations(pet, ({ one }) => ({
owner: one(person, { fields: [pet.ownerId], references: [person.id] }),
}));
export const person = onchainTable("person", (t) => ({
id: t.integer().primaryKey(),
}));
export const personRelations = relations(person, ({ many }) => ({
dogs: many(pet),
}));
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!
}
Performance tips
Here are a few tips for speeding up slow GraphQL queries.
- Create database indexes: Create indexes to speed up filters, joins, and sort conditions. Keep in mind that relations do not automatically create indexes. Read more.
- Enable horizontal scaling: If the GraphQL API is struggling to keep up with request volume, consider spreading the load across multiple instances. Read more.
- 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.
- Use pagination: Use cursor-based pagination to fetch records in smaller, more manageable chunks. This can help reduce the load on the database.