Design your schema
The schema.graphql
file contains a model of your application data. The entity types defined here can be thought of as database tables, and event handler functions are responsible for creating and updating records in those tables. The autogenerated GraphQL API is also 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 serves as a unique identifier for every 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 type unless there is a logical reason
that data might not be present. 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 | An unsigned integer (solidity uint256 ) | bigint | string |
Here's an example Account
entity type that has a field for every scalar type, and sample event handler code to insert 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 event handler code 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, and 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 inserting a Dog
entity, the owner
field is set 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 a Dog
's owner.
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 reverse lookup 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 the dogs
field on Person
cannot be accessed directly within event handler code. Fields marked with the @derivedFrom
directive are virtual, which means they are only present when querying data from the GraphQL API.
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
Entities 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.