Create & update records
The Store API is inspired by Prisma and supports the following methods.
create
Insert a new records into the store.
Options
name | type | |
---|---|---|
id | string | Hex | number | bigint | ID of the new record |
data | Record | Data required for a new record |
Returns
Promise<Record>
Examples
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Token: p.createTable({
id: p.int(),
mintedBy: p.string(),
mintedAt: p.int(),
}),
}));
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
name | type | |
---|---|---|
id | string | Hex | number | bigint | ID of the updated record |
data | Partial<Record> | Data to update |
data (function) | (args: { current: Record }) => Partial<Record> | Function returning data to update |
Returns
Promise<Record>
Examples
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Token: p.createTable({
id: p.int(),
mintedBy: p.string(),
metadataUpdatedAt: p.int(),
}),
}));
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.
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Account: p.createTable({
id: p.int(),
balance: p.bigint(),
}),
}));
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
name | type | |
---|---|---|
id | string | Hex | number | bigint | ID of the record to create or update |
create | Record | Data required for a new record |
update | Partial<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.
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")
}),
}));
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.
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Token: p.createTable({
id: p.int(),
ownedBy: p.string().references("Account.id"),
transferCount: p.int(),
}),
}));
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
name | type | |
---|---|---|
id | string | Hex | number | bigint | ID of the record to delete |
Returns
Promise<boolean>
(true
if the record was deleted, false
if it was not found)
Examples
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Player: p.createTable({
id: p.string(),
age: p.int(),
}),
}));
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
name | type | |
---|---|---|
id | string | Hex | number | bigint | ID of the record to find and return |
Returns
Promise<Record | null>
Examples
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Player: p.createTable({
id: p.string(),
age: p.int(),
}),
}));
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
name | type | |
---|---|---|
where | WhereInput | undefined | Filter for records matching a set of criteria |
orderBy | OrderByInput | undefined | Sort records by a column (default: { id: "asc" } ) |
before | string | undefined | Return records before this cursor |
after | string | undefined | Return records after this cursor |
limit | number | undefined | Number 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.
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Player: p.createTable({
id: p.string(),
age: p.int(),
}),
}));
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
.
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Player: p.createTable({
id: p.string(),
age: p.int(),
}),
}));
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.
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Player: p.createTable({
id: p.string(),
age: p.int(),
}),
}));
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"
).
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Player: p.createTable({
id: p.string(),
age: p.int(),
}),
}));
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.
name | type | |
---|---|---|
startCursor | string | null | Cursor of the first record in items |
endCursor | string | null | 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 |
Consider this setup for the following pagination examples:
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Player: p.createTable({
id: p.string(),
age: p.int(),
}),
}));
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.
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:
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
name | type | |
---|---|---|
data | Record[] | List of records to create |
Returns
Promise<Record[]>
Examples
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Player: p.createTable({
id: p.string(),
age: p.int(),
}),
}));
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
name | type | |
---|---|---|
where | WhereInput<Record> | Filter matching records to be updated |
data | Partial<Record> | Data to update |
data (function) | (args: { current: Record }) => Partial<Record> | Function returning data to update |
Returns
Promise<Record[]>
Examples
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Player: p.createTable({
id: p.string(),
age: p.int(),
}),
}));
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 }
// ]