Design your schema
The ponder.schema.ts file defines your application's database schema, and the autogenerated GraphQL API schema. Like Zod (opens in a new tab) and Drizzle (opens in a new tab), the schema definition API uses TypeScript to offer static validation and editor autocompletion throughout your app.
Here's an example ponder.schema.ts file for a simple ERC20 app.
import { p } from "@ponder/core";
export const schema = p.createSchema({
Account: p.createTable({
id: p.string(),
balance: p.bigint(),
isOwner: p.boolean(),
approvals: p.virtual("Approval.ownerId"),
transferFromEvents: p.virtual("TransferEvent.fromId"),
transferToEvents: p.virtual("TransferEvent.toId"),
}),
Approval: p.createTable({
id: p.string(),
amount: p.bigint(),
ownerId: p.string().references("Account.id"),
spender: p.string(),
}),
TransferEvent: p.createTable({
id: p.string(),
amount: p.bigint(),
fromId: p.string().references("Account.id"),
toId: p.string().references("Account.id"),
timestamp: p.int(),
}),
});Column types
To create a table, add the table name as a property in the object passed to p.createSchema(). Then, use p.createTable() and include column definitions following the same pattern.
Every table must have an id column that is a string, bytes, int, or bigint.
Primitive types
Every column is a string, bytes, int, float, bigint, or boolean. Each of these primitive types corresponds to a TypeScript type (used in indexing function code) and a JSON data type (returned by the GraphQL API).
| name | description | TypeScript type | JSON data type |
|---|---|---|---|
p.string() | A UTF‐8 character sequence | string | string |
p.bytes() | A UTF‐8 character sequence with 0x prefix | 0x${string} | string |
p.int() | A signed 32‐bit integer | number | number |
p.float() | A signed floating-point value | number | number |
p.bigint() | A signed integer (solidity int256) | bigint | string |
p.boolean() | true or false | boolean | boolean |
Here's an example Account table that has a column of every type, and a function that inserts an Account record.
import { p } from "@ponder/core";
export const schema = p.createSchema({
Account: p.createTable({
id: p.bytes(),
daiBalance: p.bigint(),
totalUsdValue: p.float(),
lastActiveAt: p.int(),
isAdmin: p.boolean(),
graffiti: p.string(),
}),
});const { Account } = context.models;
await Account.create({
id: "0xabc",
data: {
daiBalance: 7770000000000000000n,
totalUsdValue: 17.38,
lastActiveAt: 1679337733,
isAdmin: true,
graffiti: "LGTM",
},
});Enums
To define a enum, pass a list of allowable values to p.createEnum() (similar to p.createTable()). Then use p.enum() as a column type, passing the enum name as an argument. Enums use the same database and JSON types as string columns.
import { p } from "@ponder/core";
export const schema = p.createSchema({
Color: p.createEnum(["ORANGE", "BLACK"]),
Cat: p.createTable({
id: p.string(),
color: p.enum("Color"),
}),
});const { Cat } = context.models;
await Cat.create({
id: "Fluffy",
data: {
color: "ORANGE",
},
});Lists
To define a list, add .list() to any primitive or enum column. Lists should only be used for small one-dimenional collections, not relationships between records.
import { p } from "@ponder/core";
export const schema = p.createSchema({
Color: p.createEnum(["ORANGE", "BLACK"]),
FancyCat: p.createTable({
id: p.string(),
colors: p.enum("Color").list(),
favoriteNumbers: p.int().list(),
}),
});const { FancyCat } = context.models;
await FancyCat.create({
id: "Fluffy",
data: {
colors: ["ORANGE", "BLACK"],
favoriteNumbers: [7, 420, 69],
},
});Foreign keys (references)
To define a relationship between two tables, create a foreign key column. To create a foreign key column:
- Use an
Idsuffix in the column name, likeuserIdortokenId. - Add
.references("OtherTable.id")to the column type, passing the name of the referenced table.column. - Be sure that the column type matches the type of the
idcolumn of the referenced table (string,bytes,int, orbigint).
Suppose every Dog belongs to a Person. When you insert a Dog record, set the ownerId field to the id of a Person record to establish the relationship.
import { p } from "@ponder/core";
export const schema = p.createSchema({
Person: p.createTable({
id: p.string(),
age: p.int(),
}),
Dog: p.createTable({
id: p.string(),
ownerId: p.string().references("Person.id"),
}),
});const { Person, Dog } = context.models;
await Person.create({
id: "Bob",
data: { age: 22 },
});
await Dog.create({
id: "Chip",
data: { ownerId: "Bob" },
});Now, you can query for information about the owner of a Dog using the GraphQL API.
query {
dog(id: "Chip") {
id
owner {
age
}
}
}{
"dog": {
"id": "Chip",
"owner": {
"age": 22,
},
},
}Virtual columns
Virtual columns are similar to Graph Protocol derivedFrom or reverse-lookup
fields.
To create a virtual column, use p.virtual() as the column type passing the name of a column that references the current table.
import { p } from "@ponder/core";
export const schema = p.createSchema({
Person: p.createTable({
id: p.string(),
age: p.int(),
dogs: p.virtual("Dog.ownerId"),
}),
Dog: p.createTable({
id: p.string(),
ownerId: p.string().references("Person.id"),
}),
});const { Person, Dog } = context.models;
await Person.create({
id: "Bob",
});
await Dog.create({
id: "Chip",
data: { ownerId: "Bob" },
});
await Dog.create({
id: "Spike",
data: { ownerId: "Bob" },
});Now, any Dog record with ownerId: "Bob" will be present in Bob's dogs field.
query {
person(id: "Bob") {
id
dogs {
id
}
}
}{
"person": {
"id": "Bob",
"dogs": [
{ "id": "Chip" },
{ "id": "Spike" }
]
}
}You can't directly get or set the dogs field on a Person record. Virtual columns don't exist in the database. They are only present when querying data from the GraphQL API.
const { Person } = context.models;
await Person.create({
id: "Bob",
// Error, can't set a virtual column.
data: { dogs: ["Chip", "Bob"] },
});const { Person } = context.models;
const bob = await Person.get("Bob");
// `dogs` field is NOT present.
// {
// id: "Bob"
// }Optional
All columns are required, e.g. NOT NULL by default. To mark a column as optional/nullable, add .optional() to the primitive type.
import { p } from "@ponder/core";
export const schema = p.createSchema({
User: p.createTable({
id: p.bytes(),
ens: p.string(),
github: p.string().optional(),
}),
});const { User } = context.models;
await User.create({
id: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
data: {
ens: "vitalik.eth",
github: "https://github.com/vbuterin",
},
});
await User.create({
id: "0xD7029BDEa1c17493893AAfE29AAD69EF892B8ff2",
data: {
ens: "dwr.eth",
},
});