The Subgraph Schema
Lesson 8
Let's review. There are three aspects of subgraph definition. We covered how the subgraph.yaml manifest file fits into things in the last lesson. In this one, we'll go over how to build out the entities that define the subgraph schema in schema.graphql.
- subgraph.yaml - the subgraph manifest. It defines the data sources of interest and how they should be processed. NEAR is a new kind of data source.
- schema.graphql - a schema file that defines what data is stored for your subgraph, and how to query it via GraphQL.
- AssemblyScript mapping.ts - AS code that translates from the log data to the entities defined in our schema. For NEAR, there are NEAR-specific data types and JSON parsing functionality.
schema.graphql
A schema describes the structure of a resulting subgraph database and the relationships between entities. It doesn't matter what data source is used. Schemas are built using the GraphQL interface definition language.
It's important to think about how data is structured and linked. All queries are made against the data model defined in the subgraph schema and the entities indexed by the subgraph. Thus, it's a good idea to define the subgraph schema to match the needs of your application. Imagine them as "objects containing data" instead of functions. You want to structure entities around objects, not events.
We define entity types in schema.graphql and Graph Node will generate top level fields for querying single instances and collections of that entity type. Each type that is an entity gets annotated with the @entity directive.
Note
Defining entities is analogous to the process of defining the Models in a MVC (Models, Views, Controllers) framework.
For our implementation, this is super straight forward. We simply need an entity that stores all the values of the JSON object that we get from the contract log string so we can then query for them later on in our application. Remember that you or your developer ensured the contract is emitting logs in NEP 171 (JSON) format.
The Graph support for NEAR provides a handy `json.fromString` function you can use. Unless you're going to pass back the entire JSON string and work with it in the frontend, you'll definitely want to break up the object as it unlocks the power of GraphQL queries.
Relationships
You can create complex entities and relationships between them. Have a look at [Nader Dabit's tutorial on how to use `@derivedFrom` to specify a one-to-many relationship.
For one-to-many relationships, the relationship should always be stored on the 'one' side, and the 'many' side should always be derived.
Recall that our DID Registry contract is outputting logs in the following format for the `init` and `putDID` methods.
logging.log(`{"EVENT_JSON":{
"standard":"nep171",
"version":"1.0.0",
"event":"init",
"data":{
"adminId":"${adminId}",
"adminSet":${Context.blockTimestamp},
"accountId":"${adminId}"
}}}`);
logging.log(`{"EVENT_JSON":{
"standard":"nep171",
"version":"1.0.0",
"event":"putDID",
"data":{
"accountId":"${accountId}",
"did":"${did}",
"registered":${Context.blockTimestamp},
"owner":"${Context.predecessor}"
}}}`);
From our template, we've defined two entities that correspond to the data being returned in those logs - an Account entity that defines a relationship between the account and its logs and a Log entity that contains proper that reflect the types of data that can be extracted from the log JSON object.
type Account @entity {
id: ID!
signerId: String!
log: [Log!]!
}
type Log @entity {
id: ID!
standard: String!
version: String!
event: String!
adminId: String
adminSet: BigInt
accountId: String
did: String
registered: BigInt
owner: String
}
Required fields are marked with !. If it is not set in the mapping, you'll get the following error:
Null value resolved for non-null field 'name'
Each entity requires an id field which is of type ID! (string). This serves as a primary key and must be unique among all entities of the same type.
The GraphQL API supports the following scalars (single-value data types):
Type | Description |
---|---|
Bytes | Byte array represented as a hexadecimal string. Commonly used for Ethereum hashes and addresses. |
ID | Stored as a string. |
String | String values. Null characters are not supported and are automatically removed. |
Boolean |
boolean values (true or false) |
Int |
Int has size of 32 bytes |
BigInt |
Large integers. Used for Ethereum's uint32, int64, uint64, ..., uint256. Everything below uint32 is i32. |
BigDecimal |
High precision decimals represented as a significant and an exponent. Rounded to 34 significant digits. Exponent range from -6143 to 6144. |
Action Steps
With our entities defined, we need to run 'codegen' in order to generate the boilerplate code from the entities.
The 'codegen' command creates some boilerplate code under the `generated` folder. This boilerplate code defines typescript classes for each `entities` (have a look at `generated/schema.ts`). We will use this code in the next step to define the mappings between our entities and the NEAR contract logs being emitted.
Run 'codegen' like this:
$ yarn codegen
Next Steps
With the schema for our subgraph defined, we're ready to move onto the third and final file - mapping.ts. You're so very close to building and deploying your first NEAR subgraph. Keep going - on to mapping.
Maybe you'd rather have us build your NEAR subgraph for you?
Not a problem and it can be pretty quick and inexpensive depending on the complexity of your contract. Get in touch using one of the methods below.