Design your schema

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.

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 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 indexing function 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
BigIntA signed integer (solidity int256)bigintstring

Here's an example Account entity type that has a field for every scalar type, and a function that inserts 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 TypeScript 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. Lists 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 you insert a Dog entity, set the owner field 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 the owner of a Dog.

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 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").

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 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.

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

Entity types should generally be nouns

Blockchain events describe an action that has taken place (a verb). Indexing 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.