Design your schema
The schema.graphql
file contains a model of your application data. The entity types defined in schema.graphql
map to database tables, and the functions you write are responsible for creating and updating records in those tables. The GraphQL API is also autogenerated based on the entity types defined in schema.graphql
.
Entity types
Entity types are marked with the @entity
directive.
type Account @entity {
id: String! # Could also be Int!, Bytes!, or BigInt!
# ...
}
Every entity type must have an id
field that is a String!
, Int!
, Bytes!
or BigInt!
. The id
field must be a unique identifier for each instance of this entity.
Non-null types. The !
symbol is used to mark a field type as non-null.
As a general rule, prefer non-null field types unless there is a logical
reason that data might be missing. The id
field type must always be
non-null.
Scalars
In GraphQL, scalars are the most primitive types – they represent concrete data like strings and numbers. Each GraphQL scalar maps to a TypeScript type (used in event handler code) and a JSON data type (returned by the GraphQL API).
name | description | TypeScript type | JSON data type |
---|---|---|---|
String | A UTF‐8 character sequence | string | string |
Int | A signed 32‐bit integer | number | number |
Float | A signed floating-point value | number | number |
Boolean | true or false | boolean | boolean |
Bytes | A UTF‐8 character sequence with 0x prefix | 0x${string} | string |
BigInt | A signed integer (solidity int256 ) | bigint | string |
Here's an example Account
entity type that has a field for every scalar type, and a function that inserts an Account
entity.
type Account @entity {
id: Bytes!
daiBalance: BigInt!
totalUsdValue: Float!
lastActiveAt: Int!
isAdmin: Boolean!
graffiti: String!
}
await Account.create({
id: "0xabc",
data: {
daiBalance: 7770000000000000000n,
totalUsdValue: 17.38,
lastActiveAt: 1679337733,
isAdmin: true,
graffiti: "LGTM"
}
});
Enums
Enum types are a special kind of scalar that are restricted to a set of allowed values. Enum values are represented as a string
in TypeScript and in API responses.
enum Color {
ORANGE
BLACK
}
type Cat @entity {
id: String!
color: Color!
}
await Cat.create({
id: "Fluffy",
data: {
color: "ORANGE"
}
});
Basic lists
Ponder also supports lists of scalars and enums. Lists should only be used for small collections or sets of data. Lists should not be used to define relationships between entities.
enum Color {
ORANGE
BLACK
}
type FancyCat @entity {
id: String!
colors: [Color!]!
favoriteNumbers: [Int!]!
}
await FancyCat.create({
id: "Fluffy",
data: {
colors: ["ORANGE", "BLACK"],
favoriteNumbers: [7, 420, 69]
}
});
Avoid nullable list types like [Color]!
and [Color]
. See the GraphQL
docs (opens in a new tab) for more info.
One-to-one relationships
To define a one-to-one relationship, set the type of a field to another entity type. Suppose every Dog
belongs to a Person
. When you insert a Dog
entity, set the owner
field to the id
of a Person
entity. This establishes the relationship.
type Dog @entity {
id: String!
owner: Person!
}
type Person @entity {
id: String!
age: Int!
}
await Person.create({
id: "Bob",
data: { age: 22 }
});
await Dog.create({
id: "Chip",
data: { owner: "Bob" }
});
Now, you can use GraphQL to query for information about the owner of a Dog
.
query {
dog(id: "Chip") {
id
owner {
age
}
}
}
{
"dog": {
"id": "Chip",
"owner": {
"age": 22,
},
},
}
One-to-many relationships
Now, suppose a Person
can have many dogs. The @derivedFrom
directive can be used to define a one-to-many relationship between two entities. In this case, we can add a dogs
field on Person
with the type [Dog!]! @derivedFrom(field: "owner")
.
type Dog @entity {
id: String!
owner: Person!
}
type Person @entity {
id: String!
dogs: [Dog!]! @derivedFrom(field: "owner")
}
await Person.create({
id: "Bob"
});
await Dog.create({
id: "Chip",
data: { owner: "Bob" }
});
await Dog.create({
id: "Spike",
data: { owner: "Bob" }
});
Now, any Dog
entities with owner: "Bob"
will be present in Bob's dogs
field.
query {
person(id: "Bob") {
dogs {
id
}
}
}
{
"person": {
"dogs": [
{ "id": "Chip" },
{ "id": "Spike" },
]
},
}
Note that you cannot directly get or set the dogs
field on Person
. Fields marked with the @derivedFrom
directive are virtual, which means they are only present when querying data from the GraphQL API. This pattern is also known as a reverse lookup, because the connection is defined on the "many" side of the relationship.
await Person.create({
id: "Bob",
data: {
dogs: ["Chip", "Bob"] // WRONG, will throw an error.
}
});
const bob = await Person.get("Bob");
// `dogs` field is NOT present.
// {
// id: "Bob"
// }
Schema design tips
Entity types should generally be nouns
Blockchain events describe an action that has taken place (a verb). Event handler funtions are most effective when they convert events into the nouns that represent the current state of your application. For example, prefer modeling an ERC721 collection using Account
and Token
entities rather than TransferEvent
entities. (Unless you need the full transfer history of every account).
Use relationship types generously
Your schema will be more flexible and powerful if it accurately models the logical relationships in your application's domain. Don't use the basic list type to store entity IDs or to model relationships.