302 lines
5.8 KiB
Markdown
302 lines
5.8 KiB
Markdown
# Entity Patterns in HyperIndex
|
|
|
|
Patterns for defining and working with entities in HyperIndex.
|
|
|
|
## Schema Definition
|
|
|
|
### Basic Entity
|
|
|
|
```graphql
|
|
type Token {
|
|
id: ID!
|
|
name: String!
|
|
symbol: String!
|
|
decimals: BigInt!
|
|
totalSupply: BigInt!
|
|
}
|
|
```
|
|
|
|
**Key differences from TheGraph:**
|
|
- No `@entity` decorator needed
|
|
- Use `String!` instead of `Bytes!`
|
|
- Use `BigInt!` for numbers (not `BigDecimal!` in schema)
|
|
|
|
### Entity Relationships
|
|
|
|
Use `_id` suffix for relationships:
|
|
|
|
```graphql
|
|
type Transfer {
|
|
id: ID!
|
|
from: String!
|
|
to: String!
|
|
amount: BigInt!
|
|
token_id: String! # References Token.id
|
|
blockNumber: BigInt!
|
|
}
|
|
|
|
type Token {
|
|
id: ID!
|
|
name: String!
|
|
symbol: String!
|
|
transfers: [Transfer!]! @derivedFrom(field: "token")
|
|
}
|
|
```
|
|
|
|
**Critical:** Entity arrays MUST have `@derivedFrom`:
|
|
|
|
```graphql
|
|
# WRONG - Will fail codegen
|
|
type Transaction {
|
|
mints: [Mint!]! # Missing @derivedFrom
|
|
}
|
|
|
|
# CORRECT
|
|
type Transaction {
|
|
id: ID!
|
|
mints: [Mint!]! @derivedFrom(field: "transaction")
|
|
}
|
|
|
|
type Mint {
|
|
id: ID!
|
|
transaction_id: String! # The relationship field
|
|
}
|
|
```
|
|
|
|
### Optional Fields
|
|
|
|
Use nullable types for optional fields:
|
|
|
|
```graphql
|
|
type Token {
|
|
id: ID!
|
|
name: String!
|
|
symbol: String!
|
|
logoUrl: String # Optional - no !
|
|
}
|
|
```
|
|
|
|
**Important:** Use `undefined` not `null` in TypeScript:
|
|
|
|
```typescript
|
|
const token = {
|
|
id: "0x...",
|
|
name: "Token",
|
|
symbol: "TKN",
|
|
logoUrl: undefined, // Not null
|
|
};
|
|
```
|
|
|
|
## Creating Entities
|
|
|
|
### Basic Creation
|
|
|
|
```typescript
|
|
MyContract.Transfer.handler(async ({ event, context }) => {
|
|
const transfer = {
|
|
id: `${event.chainId}-${event.transaction.hash}-${event.logIndex}`,
|
|
from: event.params.from,
|
|
to: event.params.to,
|
|
amount: event.params.amount,
|
|
token_id: event.srcAddress, // Relationship
|
|
blockNumber: BigInt(event.block.number),
|
|
};
|
|
|
|
context.Transfer.set(transfer);
|
|
});
|
|
```
|
|
|
|
### With Multichain ID
|
|
|
|
Always prefix IDs with chainId for multichain:
|
|
|
|
```typescript
|
|
const id = `${event.chainId}-${event.params.tokenId}`;
|
|
```
|
|
|
|
### Entity ID Patterns
|
|
|
|
```typescript
|
|
// Event-based (unique per event)
|
|
`${event.chainId}-${event.transaction.hash}-${event.logIndex}`
|
|
|
|
// Address-based (singleton per address per chain)
|
|
`${event.chainId}-${event.srcAddress}`
|
|
|
|
// Composite (multiple keys)
|
|
`${event.chainId}-${event.params.user}-${event.params.tokenId}`
|
|
|
|
// Time-based (daily aggregates)
|
|
`${event.chainId}-${dayTimestamp}-${event.srcAddress}`
|
|
```
|
|
|
|
## Updating Entities
|
|
|
|
**Entities are immutable.** Use spread operator for updates:
|
|
|
|
```typescript
|
|
MyContract.Transfer.handler(async ({ event, context }) => {
|
|
const token = await context.Token.get(event.srcAddress);
|
|
|
|
if (token) {
|
|
// CORRECT - Use spread operator
|
|
const updatedToken = {
|
|
...token,
|
|
totalSupply: token.totalSupply + event.params.amount,
|
|
lastUpdated: BigInt(event.block.timestamp),
|
|
};
|
|
context.Token.set(updatedToken);
|
|
}
|
|
});
|
|
```
|
|
|
|
**Never mutate directly:**
|
|
|
|
```typescript
|
|
// WRONG - Entities are read-only
|
|
token.totalSupply = newSupply;
|
|
context.Token.set(token); // Won't work
|
|
```
|
|
|
|
## Loading Entities
|
|
|
|
### Get by ID
|
|
|
|
```typescript
|
|
const token = await context.Token.get(tokenId);
|
|
if (token) {
|
|
// Token exists
|
|
}
|
|
```
|
|
|
|
### Get or Create Pattern
|
|
|
|
```typescript
|
|
MyContract.Transfer.handler(async ({ event, context }) => {
|
|
let token = await context.Token.get(event.srcAddress);
|
|
|
|
if (!token) {
|
|
token = {
|
|
id: event.srcAddress,
|
|
name: "Unknown",
|
|
symbol: "???",
|
|
decimals: BigInt(18),
|
|
totalSupply: BigInt(0),
|
|
};
|
|
}
|
|
|
|
const updatedToken = {
|
|
...token,
|
|
totalSupply: token.totalSupply + event.params.amount,
|
|
};
|
|
|
|
context.Token.set(updatedToken);
|
|
});
|
|
```
|
|
|
|
## Querying Related Entities
|
|
|
|
`@derivedFrom` arrays are virtual - cannot access in handlers:
|
|
|
|
```typescript
|
|
// WRONG - derivedFrom arrays don't exist in handlers
|
|
const transfers = token.transfers;
|
|
|
|
// CORRECT - Query using indexed field
|
|
// (If using indexed field operations)
|
|
const transfers = await context.Transfer.getWhere.token_id.eq(tokenId);
|
|
```
|
|
|
|
## BigDecimal Handling
|
|
|
|
For precision in calculations, use BigDecimal:
|
|
|
|
```typescript
|
|
import { BigDecimal } from "generated";
|
|
|
|
const ZERO_BD = new BigDecimal(0);
|
|
const ONE_BD = new BigDecimal(1);
|
|
|
|
// Convert token amount to decimal
|
|
function convertToDecimal(amount: bigint, decimals: bigint): BigDecimal {
|
|
const divisor = new BigDecimal(10n ** decimals);
|
|
return new BigDecimal(amount.toString()).div(divisor);
|
|
}
|
|
```
|
|
|
|
**Schema types vs code types:**
|
|
- Schema `BigInt!` → TypeScript `bigint`
|
|
- Schema `BigDecimal!` → TypeScript `BigDecimal`
|
|
- Schema `Int!` → TypeScript `number`
|
|
|
|
## Timestamp Handling
|
|
|
|
Always cast timestamps:
|
|
|
|
```typescript
|
|
// CORRECT
|
|
timestamp: BigInt(event.block.timestamp)
|
|
|
|
// For day calculations
|
|
const dayTimestamp = Math.floor(event.block.timestamp / 86400) * 86400;
|
|
const dayId = `${event.chainId}-${dayTimestamp}`;
|
|
```
|
|
|
|
## Common Entity Types
|
|
|
|
### Token Entity
|
|
|
|
```graphql
|
|
type Token {
|
|
id: ID!
|
|
address: String!
|
|
name: String!
|
|
symbol: String!
|
|
decimals: BigInt!
|
|
totalSupply: BigInt!
|
|
transfers: [Transfer!]! @derivedFrom(field: "token")
|
|
}
|
|
```
|
|
|
|
### Transfer Entity
|
|
|
|
```graphql
|
|
type Transfer {
|
|
id: ID!
|
|
token_id: String!
|
|
from: String!
|
|
to: String!
|
|
amount: BigInt!
|
|
blockNumber: BigInt!
|
|
blockTimestamp: BigInt!
|
|
transactionHash: String!
|
|
}
|
|
```
|
|
|
|
### Daily Aggregate Entity
|
|
|
|
```graphql
|
|
type TokenDayData {
|
|
id: ID! # chainId-dayTimestamp-tokenAddress
|
|
token_id: String!
|
|
date: Int! # Unix timestamp of day start
|
|
volume: BigDecimal!
|
|
txCount: BigInt!
|
|
open: BigDecimal!
|
|
high: BigDecimal!
|
|
low: BigDecimal!
|
|
close: BigDecimal!
|
|
}
|
|
```
|
|
|
|
## Entity Checklist
|
|
|
|
When defining entities:
|
|
- [ ] Use `ID!` for id field
|
|
- [ ] Use `String!` for addresses (not `Bytes!`)
|
|
- [ ] Use `_id` suffix for relationships
|
|
- [ ] Add `@derivedFrom` to all entity arrays
|
|
- [ ] No `@entity` decorator
|
|
- [ ] Consider multichain ID prefixes
|
|
- [ ] Match field types exactly (BigInt vs BigDecimal vs Int)
|