Design your schema

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.

schema.graphql
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).

namedescriptionTypeScript typeJSON data type
StringA UTF‐8 character sequencestringstring
IntA signed 32‐bit integernumbernumber
FloatA signed floating-point valuenumbernumber
Booleantrue or falsebooleanboolean
BytesA UTF‐8 character sequence with 0x prefix0x${string}string
BigIntAn unsigned integer (solidity uint256)bigintstring

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.

schema.graphql
type Account @entity {
  id: Bytes!
  daiBalance: BigInt!
  totalUsdValue: Float!
  lastActiveAt: Int!
  isAdmin: Boolean!
  graffiti: String!
}
src/index.ts
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.

schema.graphql
enum Color {
  ORANGE
  BLACK
}
 
type Cat @entity {
  id: String!
  color: Color!
}
src/index.ts
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.

schema.graphql
enum Color {
  ORANGE
  BLACK
}
 
type FancyCat @entity {
  id: String!
  colors: [Color!]!
  favoriteNumbers: [Int!]!
}
src/index.ts
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.

schema.graphql
type Dog @entity {
  id: String!
  owner: Person!
}
 
type Person @entity {
  id: String!
  age: Int!
}
src/index.ts
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
query {
  dog(id: "Chip") {
    id
    owner {
      age
    }
  }
}
Result
{
  "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").

schema.graphql
type Dog @entity {
  id: String!
  owner: Person!
}
 
type Person @entity {
  id: String!
  dogs: [Dog!]! @derivedFrom(field: "owner")
}
src/index.ts
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
query {
  person(id: "Bob") {
    dogs {
      id
    }
  }
}
Result
{
  "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.

src/index.ts
await Person.create({
  id: "Bob",
  data: {
    dogs: ["Chip", "Bob"] // WRONG, will throw an error.
  }
});
src/index.ts
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.