Initial commit
This commit is contained in:
301
skills/hyperindex-development/references/entity-patterns.md
Normal file
301
skills/hyperindex-development/references/entity-patterns.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user