Entities Schema (schema.graphql)
The schema.graphql file defines the data model for your HyperIndex indexer. Each entity type defined in this schema corresponds directly to a database table, with your event handlers responsible for creating and updating the records. HyperIndex automatically generates a GraphQL API based on these entity types, allowing easy access to the indexed data.
Defining Entity Types
Entities in your schema are defined as GraphQL object types:
Example:
type User {
id: ID!
greetings: [String!]!
latestGreeting: String!
numberOfGreetings: Int!
}
Requirements:
- Every entity must have a unique
idfield, using one of these scalar types:ID!,String!,Int!, orBigInt!
- The
idfield must be non-nullable, must not be a list, and cannot be a@derivedFromfield.
Numeric Entity IDs
ID is the usual choice and behaves as a string. Since v3.5, you can also key an entity on Int or BigInt, which is a better fit when the identifier is genuinely a number — a block number, an auction id, a sequential position:
type Auction {
id: BigInt! # the on-chain auction id, not a stringified copy of it
seller: String!
bids: [Bid!]! @derivedFrom(field: "auction")
}
type Bid {
id: ID!
auction: Auction! # inferred as BigInt to match Auction.id
amount: BigInt!
}
Relationship fields adopt the referenced entity's id type automatically, so Bid.auction above is typed bigint in your handlers rather than string. You don't declare the foreign key type — keep the two sides in sync by changing the referenced entity's id.
Scalar Types
Scalar types represent basic data types and map directly to JavaScript, TypeScript, or ReScript types.
| GraphQL Scalar | Description | JavaScript/TypeScript | ReScript |
|---|---|---|---|
ID | Unique identifier | string | string |
String | UTF-8 character sequence | string | string |
Int | Signed 32-bit integer | number | int |
Float | Signed floating-point number | number | float |
Boolean | true or false | boolean | bool |
Bytes | UTF-8 character sequence (hex prefixed 0x) | string | string |
BigInt | Signed integer (int256 in Solidity) | bigint | bigint |
BigDecimal | Arbitrary-size floating-point | BigDecimal (imported) | BigDecimal.t |
Timestamp | Timestamp with timezone | Date | Js.Date.t |
Json | JSON object | Json | Js.Json.t |
Learn more about GraphQL scalars here.
Working with BigDecimal
The BigDecimal scalar type in HyperIndex is based on the bignumber.js library, which provides arbitrary-precision decimal arithmetic. This is essential for financial calculations and handling numeric values that exceed JavaScript's native number precision.
Importing BigDecimal
// JavaScript/TypeScript
import { BigDecimal } from "envio";
// ReScript
open BigDecimal;
Creating BigDecimal Instances
// From string (recommended for precision)
const price = new BigDecimal("123.456789");
// From number (may lose precision for very large values)
const amount = new BigDecimal(123.45);
// From other BigDecimal
const copy = new BigDecimal(price);
Arithmetic Operations
BigDecimal instances are immutable. Operations return new BigDecimal instances:
// Basic arithmetic
const a = new BigDecimal("123.45");
const b = new BigDecimal("67.89");
const sum = a.plus(b); // 191.34
const difference = a.minus(b); // 55.56
const product = a.times(b); // 8,381.03
const quotient = a.div(b); // 1.81839...
// Power
const squared = a.pow(2); // 15,239.9025
// Square root
const root = a.sqrt(); // 11.11...
// Absolute value
const abs = new BigDecimal("-123.45").abs(); // 123.45
Comparison Methods
const x = new BigDecimal("10.5");
const y = new BigDecimal("10.5");
const z = new BigDecimal("9.9");
x.eq(y); // true (equal)
x.gt(z); // true (greater than)
x.gte(y); // true (greater than or equal)
x.lt(z); // false (less than)
x.lte(y); // true (less than or equal)
// Check for special values
x.isZero(); // false
x.isPositive(); // true
x.isNegative(); // false
x.isFinite(); // true
Rounding and Formatting
const value = new BigDecimal("123.456789");
// Get with specific decimal places
value.dp(2); // 123.46 (rounded)
value.dp(2, 1); // 123.45 (rounded down)
// Format as string
value.toString(); // "123.456789"
value.toFixed(2); // "123.46"
value.toExponential(2); // "1.23e+2"
value.toPrecision(5); // "123.46"
Working with Schema-Defined BigDecimal Fields
When you've defined a BigDecimal field in your schema:
type TokenPair {
id: ID!
name: String!
price: BigDecimal!
volume: BigDecimal!
}
You can use it in your handlers:
// In your event handler
context.TokenPair.set({
id: event.params.pairId,
name: event.params.name,
price: new BigDecimal(event.params.price),
volume: new BigDecimal("0"), // Start with zero volume
});
// Updating a field
const tokenPair = await context.TokenPair.get(pairId);
if (tokenPair) {
const newVolume = tokenPair.volume.plus(new BigDecimal(tradeAmount));
context.TokenPair.set({
...tokenPair,
volume: newVolume,
});
}
Example: Financial Calculation
function calculateFee(amount: BigDecimal, feeRate: BigDecimal): BigDecimal {
// Calculate fee with proper rounding
return amount.times(feeRate).dp(2);
}
const tradeAmount = new BigDecimal("1250.75");
const feeRate = new BigDecimal("0.0025"); // 0.25%
const fee = calculateFee(tradeAmount, feeRate); // 3.13
Best Practices for BigDecimal
-
Always use strings for initialization when precision matters:
// Preferred
const value = new BigDecimal("123.456789");
// May lose precision
const value = new BigDecimal(123.456789); -
Set precision explicitly when doing division:
// Set to 8 decimal places for crypto prices
const price = totalValue.div(tokenAmount).dp(8); -
Handle rounding appropriately for financial calculations:
// Round down (floor) for user-favorable calculations
const userReceives = amount.dp(2, 1); // ROUND_DOWN
// Round up (ceil) for protocol-favorable calculations
const protocolFee = amount.dp(2, 0); // ROUND_UP -
Compare with equals method instead of
==or===:// Correct
if (value.eq(new BigDecimal(0))) {
/* ... */
}
// Incorrect - compares object references
if (value === new BigDecimal(0)) {
/* ... */
} -
Chain operations carefully, remembering that each operation returns a new instance:
// Calculate (a + b) * c with proper precision
const result = a.plus(b).times(c).dp(8);
Enum Types
Enums allow fields to accept only a predefined set of values.
Example:
enum AccountType {
ADMIN
USER
}
type User {
id: ID!
balance: Int!
accountType: AccountType!
}
Enums translate to string unions (TypeScript/JavaScript) or polymorphic variants (ReScript):
TypeScript Example:
import { type Enum } from "envio";
let user = {
id: event.params.id,
balance: event.params.balance,
accountType: "USER" satisfies Enum<"AccountType">, // enum as string
};
ReScript Example:
let user: Types.userEntity = {
id: event.params.id,
balance: event.params.balance,
accountType: #USER, // polymorphic variant
};
Relationships: One-to-Many (@derivedFrom)
Define relationships between entities using the @derivedFrom directive, known as reverse lookups.
Example:
type NftCollection {
id: ID!
contractAddress: Bytes!
name: String!
symbol: String!
maxSupply: BigInt!
currentSupply: Int!
tokens: [Token!]! @derivedFrom(field: "collection")
}
type Token {
id: ID!
tokenId: BigInt!
collection: NftCollection!
owner: User!
}
- The
tokensfield inNftCollectionis a virtual field, populated automatically when querying the API. - Set relationships in your handlers by assigning
<field>_idwith the related entity'sid. For example, create or update aTokenentity withcollection_id: collectionId.
Field Indexing (@index)
Add an index to a field for optimized queries and loader performance:
type Token {
id: ID!
tokenId: BigInt!
collection: NftCollection!
owner: User! @index
}
- All
idfields and fields referenced via@derivedFromare indexed automatically. - Declare
@indexfor the fields your GraphQL consumers filter and sort by. You don't need it for fields your handlers query withgetWhere— since v3.5 HyperIndex creates those indices itself. See Indices created on demand.
Choosing a Storage Backend (@storage)
When you enable more than one storage backend in config.yaml, the @storage directive controls where each entity is written:
# Queryable over GraphQL and mirrored into ClickHouse for analytics
type Transfer @storage(postgres: true, clickhouse: true) {
id: ID!
amount: BigInt!
}
Since v3.2 you can mark a backend as default in config.yaml, and entities without a @storage directive go there — you no longer need the directive on every entity. See storage.
Per-Entity ClickHouse Tuning
Since v3.4, the clickhouse argument also accepts an options object that tunes that entity's ClickHouse history table:
type Transfer
@storage(
postgres: true
clickhouse: {
partitionBy: "toYYYYMM(timestamp)"
orderBy: ["timestamp"]
ttl: "timestamp + INTERVAL 2 YEAR"
}
) {
id: ID!
timestamp: Timestamp!
amount: BigInt!
}
| Option | Type | Description |
|---|---|---|
partitionBy | ClickHouse expression | Emitted as PARTITION BY <expr>. Keeps queries and TTL deletes inside a partition instead of scanning the whole table. |
orderBy | list of entity field names | Fields that lead the table's sorting key, ahead of the default id. |
ttl | ClickHouse expression | Emitted as TTL <expr>. Ages rows out automatically. |
A few constraints, all caught at envio codegen rather than at runtime:
orderBytakes entity field names, not expressions — unlikepartitionByandttl, which are ClickHouse expressions passed through as written.orderBycan't listid(already the default sorting key), nor nullable, list or@derivedFromfields, which ClickHouse doesn't allow in a sorting key.- An entity can carry only one
@storagedirective, and it must enable at least one backend.
Advanced: Precision and Scale (@config Directive)
Customize the precision and scale for BigInt and BigDecimal fields using @config.
Syntax:
BigInt(precision only):
amount: BigInt @config(precision: 76)
BigDecimal(precision and scale):
price: BigDecimal @config(precision: 10, scale: 2)
Example:
type Payment {
id: ID!
amount: BigInt @config(precision: 76)
price: BigDecimal @config(precision: 10, scale: 2)
}
This controls PostgreSQL storage allocation and numerical accuracy.
Detailed Example with Arrays
type AdvancedEntity {
exampleBigInt: BigInt @config(precision: 76)
exampleBigIntRequired: BigInt! @config(precision: 77)
exampleBigIntArray: [BigInt!] @config(precision: 78)
exampleBigIntArrayRequired: [BigInt!]! @config(precision: 79)
exampleBigDecimal: BigDecimal @config(precision: 10, scale: 5)
exampleBigDecimalRequired: BigDecimal! @config(precision: 12, scale: 4)
}
Documenting Entities, Fields, and Relationships
You can document your entities, fields, and relationships directly in schema.graphql using GraphQL string descriptions. These descriptions are exposed through the generated GraphQL API and appear in introspection, making your API self-documenting.
"""
A token transfer between two accounts
"""
type Transfer {
id: ID!
"The address the tokens were sent from"
from: String!
"The address the tokens were sent to"
to: String!
"The amount transferred, in wei"
value: BigInt!
}
Both single-line ("...") and multi-line ("""...""") descriptions are supported.
Only string descriptions are exposed in introspection. Hash (#) comments are ignored by the GraphQL parser and do not appear in the API. Descriptions on entities, fields, and relationships were added in HyperIndex v3.1.
Generating Types
Once you've defined your schema, run this command to generate these entity types that can be accessed in your event handlers:
pnpm envio codegen
Best Practices
- Use camelCase for field names (
latestGreeting,numberOfGreetings). - Keep entity and field names clear, descriptive, and intuitive.
You're now ready to define powerful schemas and efficiently query your indexed data with HyperIndex!