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.