Documentation
Write indexing functions
Create & update records

Create & update records

Ponder's store API is inspired by the Prisma Client API. The store supports the following methods.

create

Insert a new records into the store.

Options

nametype
idstring | Hex | number | bigintID of the new record
dataRecordData required for a new record

Returns

Promise<Record>

Examples

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Token: p.createTable({
    id: p.int(),
    mintedBy: p.string(),
    mintedAt: p.int(),
  }),
}));
src/index.ts
ponder.on("Blitmap:Mint", async ({ event, context }) => {
  const { Token } = context.db;
 
  const token = await Token.create({
    id: event.args.tokenId,
    data: {
      mintedBy: event.args.to,
      mintedAt: event.block.timestamp,
    },
  });
  // { id: 7777, mintedBy: "0x7Df1...", mintedAt: 1679507353 }
});

update

Update an record that already exists.

Options

nametype
idstring | Hex | number | bigintID of the updated record
dataPartial<Record>Data to update
data (function)(args: { current: Record }) => Partial<Record>Function returning data to update

Returns

Promise<Record>

Examples

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Token: p.createTable({
    id: p.int(),
    mintedBy: p.string(),
    metadataUpdatedAt: p.int(),
  }),
}));
src/index.ts
ponder.on("Blitmap:MetadataUpdate", async ({ event, context }) => {
  const { Token } = context.db;
 
  const token = await Token.update({
    id: event.args.tokenId,
    data: {
      metadataUpdatedAt: event.block.timestamp,
    },
  });
  // { id: 7777, mintedBy: "0x1bA3...", updatedAt: 1679507354 }
});

Update function

You can optionally pass a function to the data field that receives the current record as an argument and returns the update object. This is useful for updates that depend on the current record, like an incrementing count or balance.

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Account: p.createTable({
    id: p.int(),
    balance: p.bigint(),
  }),
}));
src/index.ts
ponder.on("ERC20:Transfer", async ({ event, context }) => {
  const { Account } = context.db;
 
  const recipient = await Account.update({
    id: event.args.to,
    data: ({ current }) => ({
      balance: current.balance + event.args.value,
    }),
  });
  // { id: "0x5D92..", balance: 11800000005n }
});

upsert

Update a record if one already exists with the specified id, or create a new record.

Options

nametype
idstring | Hex | number | bigintID of the record to create or update
createRecordData required for a new record
updatePartial<Record>Data to update
update (function)(args: { current: Record }) => Partial<Record>Function returning data to update

Returns

Promise<Record>

Examples

Upsert can be useful for events like the ERC721 Transfer event, which is emitted when a token is minted and whenever a token is transferred.

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Token: p.createTable({
    id: p.int(),
    mintedBy: p.string().references("Account.id")
    ownedBy: p.string().references("Account.id")
  }),
}));
src/index.ts
ponder.on("Blitmap:Transfer", async ({ event, context }) => {
  const { Token } = context.db;
 
  const token = await Token.upsert({
    id: event.args.tokenId,
    create: {
      mintedBy: event.args.to,
      ownedBy: event.args.to,
    },
    update: {
      ownedBy: event.args.to,
    },
  });
  // { id: 7777, mintedBy: "0x1bA3...", ownedBy: "0x7F4d..." }
});

Update function

You can optionally pass a function to the update field that receives the current record as an argument and returns the update object. This is useful for updates that depend on the current record, like an incrementing count or balance.

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Token: p.createTable({
    id: p.int(),
    ownedBy: p.string().references("Account.id"),
    transferCount: p.int(),
  }),
}));
src/index.ts
ponder.on("Blitmap:Transfer", async ({ event, context }) => {
  const { Token } = context.db;
 
  const token = await Token.upsert({
    id: event.args.tokenId,
    create: {
      ownedBy: event.args.to,
      transferCount: 0,
    },
    update: ({ current }) => ({
      ownedBy: event.args.to,
      transferCount: current.transferCount + 1,
    }),
  });
  // { id: 7777, ownedBy: "0x7F4d...", transferCount: 1 }
});

delete

delete deletes a record by id.

Options

nametype
idstring | Hex | number | bigintID of the record to delete

Returns

Promise<boolean> (true if the record was deleted, false if it was not found)

Examples

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Player: p.createTable({
    id: p.string(),
    age: p.int(),
  }),
}));
src/index.ts
await Player.create({
  id: "Jim",
  data: { age: 34 },
});
 
const isDeleted = await Player.delete({
  id: "Jim",
});
// true
 
const jim = await Player.findUnique({
  id: "Jim",
});
// null

findUnique

findUnique finds and returns a record by id.

Options

nametype
idstring | Hex | number | bigintID of the record to find and return

Returns

Promise<Record | null>

Examples

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Player: p.createTable({
    id: p.string(),
    age: p.int(),
  }),
}));
src/index.ts
await Player.create({
  id: "Jim",
  data: { age: 34 },
});
 
const jim = await Player.findUnique({
  id: "Jim",
});
// { id: "Jim", age: 34 }
 
const sara = await Player.findUnique({
  id: "Sara",
});
// null

findMany

findMany returns a list of records according to the filter, sort, and pagination options you provide. Note that findMany offers programmatic access to the functionality exposed by the autogenerated GraphQL API.

Options

nametype
whereWhereInput | undefinedFilter for records matching a set of criteria
orderByOrderByInput | undefinedSort records by a column (default: { id: "asc" })
beforestring | undefinedReturn records before this cursor
afterstring | undefinedReturn records after this cursor
limitnumber | undefinedNumber of records to return (default: 50, max: 1000)

Returns

Promise<{ items: Record[], pageInfo: PageInfo }>

Examples

Filtering

Filter the result list by passing a where option containing a field name, filter condition, and value. The where option is typed according to the filter conditions available for each field.

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Player: p.createTable({
    id: p.string(),
    age: p.int(),
  }),
}));
src/index.ts
await Player.createMany({
  data: [
    { id: "Jim", age: 34 },
    { id: "Andrew", age: 19 },
    { id: "Janet", age: 56 },
  ],
});
 
const { items } = await Player.findMany();
// [
//   { id: "Jim", age: 34 },
//   { id: "Andrew", age: 19 },
//   { id: "Janet", age: 56 }
// ]
 
const { items } = await Player.findMany({
  where: {
    id: {
      startsWith: "J",
    },
  },
});
// [
//   { id: "Jim", age: 34 },
//   { id: "Janet", age: 56 }
// ]

If you provide multiple filters, they will be combined with a logical AND.

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Player: p.createTable({
    id: p.string(),
    age: p.int(),
  }),
}));
src/index.ts
await Player.createMany({
  data: [
    { id: "Jim", age: 34 },
    { id: "Andrew", age: 19 },
    { id: "Janet", age: 56 },
  ],
});
 
const { items } = await Player.findMany({
  where: {
    id: { contains: "e" }
    age: { gt: 30 }
  }
});
// [
//   { id: "Janet", age: 56 }
// ]

You can also use the AND and OR operators to combine filters.

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Player: p.createTable({
    id: p.string(),
    age: p.int(),
  }),
}));
src/index.ts
await Player.createMany({
  data: [
    { id: "Jim", age: 34 },
    { id: "Andrew", age: 19 },
    { id: "Janet", age: 56 },
  ],
});
 
const { items } = await Player.findMany({
  where: {
    OR: [
      { id: { endsWith: "m" } },
      { age: { gt: 50 } },
    ],
  }
});
// [
//   { id: "Jim", age: 34 },
//   { id: "Janet", age: 56 }
// ]

Sorting

Sort the result list by passing an orderBy option containing a field name and sort direction ("asc" or "desc").

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Player: p.createTable({
    id: p.string(),
    age: p.int(),
  }),
}));
src/index.ts
await Player.createMany({
  data: [
    { id: "Jim", age: 34 },
    { id: "Andrew", age: 19 },
    { id: "Janet", age: 56 },
  ],
});
 
const { items } = await Player.findMany({
  orderBy: {
    age: "asc",
  },
});
// [
//   { id: "Andrew", age: 19 },
//   { id: "Jim", age: 34 },
//   { id: "Janet", age: 56 }
// ]

Pagination

The findMany cursor pagination API is 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.

PageInfo type.

nametype
startCursorstring | nullCursor of the first record in items
endCursorstring | nullCursor of the last record in items
hasPreviousPagebooleanWhether there are more records before this page
hasNextPagebooleanWhether there are more records after this page

Consider this setup for the following pagination examples:

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Player: p.createTable({
    id: p.string(),
    age: p.int(),
  }),
}));
src/index.ts
await Player.createMany({
  data: [
    { id: "Jim", age: 34 },
    { id: "Andrew", age: 19 },
    { id: "Janet", age: 56 },
    { id: "Polly", age: 29 },
  ],
});

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

src/index.ts
const pageOne = await Player.findMany({
  orderBy: { age: "asc" },
  limit: 2,
});
// {
//   items: [ { id: "Andrew", age: 19 }, { id: "Polly", age: 29 } ],
//   pageInfo: {
//     startCursor: "MfgBzeDkjs44",
//     endCursor: "Mxhc3NDb3JlLTA=",
//     hasPreviousPage: false,
//     hasNextPage: true,
//   }
// }
 
const pageTwo = await Player.findMany({
  orderBy: { age: "desc" },
  after: pageOne.pageInfo.endCursor,
});
// {
//   items: [ { id: "Jim", age: 34 }, { id: "Janet", age: 56 } ],
//   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. Picking up where the previous example left off:

src/index.ts
const pageThree = await Player.findMany({
  orderBy: { age: "asc" },
  before: pageTwo.pageInfo.startCursor,
  limit: 1,
});
// {
//   items: [ { id: "Polly", age: 29 } ],
//   pageInfo: {
//     startCursor: "Mxhc3NDb3JlLTA=",
//     endCursor: "Mxhc3NDb3JlLTA=",
//     hasPreviousPage: true,
//     hasNextPage: true,
//   }
// }

createMany

createMany inserts multiple records into the store in a single operation. It returns a list of the created records.

Options

nametype
dataRecord[]List of records to create

Returns

Promise<Record[]>

Examples

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Player: p.createTable({
    id: p.string(),
    age: p.int(),
  }),
}));
src/index.ts
await Player.createMany({
  data: [
    { id: "Jim", age: 34 },
    { id: "Andrew", age: 19 },
    { id: "Janet", age: 56 },
  ],
});
 
const players = await Player.findMany();
// [
//   { id: "Jim", age: 34 },
//   { id: "Andrew", age: 19 },
//   { id: "Janet", age: 56 }
// ]

updateMany

updateMany updates multiple records in a single operation using the same update logic. Like the update method, updateMany also optionally accepts an update function.

Options

nametype
whereWhereInput<Record>Filter matching records to be updated
dataPartial<Record>Data to update
data (function)(args: { current: Record }) => Partial<Record>Function returning data to update

Returns

Promise<Record[]>

Examples

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Player: p.createTable({
    id: p.string(),
    age: p.int(),
  }),
}));
src/index.ts
await Player.createMany({
  data: [
    { id: "Jim", age: 34 },
    { id: "Andrew", age: 19 },
    { id: "Janet", age: 56 },
  ],
});
 
await Player.updateMany({
  where: {
    id: {
      startsWith: "J",
    },
  },
  data: {
    age: 50,
  },
});
 
const players = await Player.findMany();
// [
//   { id: "Jim", age: 50 },
//   { id: "Andrew", age: 19 },
//   { id: "Janet", age: 50 }
// ]