328 lines
6.8 KiB
Markdown
328 lines
6.8 KiB
Markdown
# GraphQL Querying
|
|
|
|
Query indexed data via GraphQL. Works locally during development or on hosted deployments.
|
|
|
|
**Local endpoint:** `http://localhost:8080/v1/graphql`
|
|
**Hasura Console:** `http://localhost:8080` (password: `testing`)
|
|
|
|
## Checking Indexing Progress
|
|
|
|
**Always check sync status first** before assuming data is missing.
|
|
|
|
### Using `_meta` (Recommended)
|
|
|
|
```bash
|
|
curl -s http://localhost:8080/v1/graphql \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query": "{ _meta { chainId startBlock progressBlock sourceBlock eventsProcessed isReady } }"}'
|
|
```
|
|
|
|
Or in GraphQL:
|
|
```graphql
|
|
{
|
|
_meta {
|
|
chainId
|
|
startBlock
|
|
progressBlock
|
|
sourceBlock
|
|
eventsProcessed
|
|
isReady
|
|
readyAt
|
|
}
|
|
}
|
|
```
|
|
|
|
**Fields:**
|
|
- `progressBlock` - Last fully processed block
|
|
- `sourceBlock` - Latest known block from data source (target)
|
|
- `eventsProcessed` - Total events processed
|
|
- `isReady` - `true` when historical sync is complete
|
|
- `readyAt` - Timestamp when sync finished
|
|
|
|
**Example response:**
|
|
```json
|
|
{
|
|
"_meta": [
|
|
{
|
|
"chainId": 1,
|
|
"progressBlock": 22817138,
|
|
"sourceBlock": 23368264,
|
|
"eventsProcessed": 2380000,
|
|
"isReady": false
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Filter by Chain
|
|
|
|
```graphql
|
|
{
|
|
_meta(where: { chainId: { _eq: 1 } }) {
|
|
progressBlock
|
|
isReady
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using `chain_metadata`
|
|
|
|
More detailed chain information:
|
|
|
|
```bash
|
|
curl -s http://localhost:8080/v1/graphql \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query": "{ chain_metadata { chain_id start_block latest_processed_block num_events_processed is_hyper_sync } }"}'
|
|
```
|
|
|
|
**Additional fields:**
|
|
- `is_hyper_sync` - Whether using HyperSync (fast) or RPC
|
|
- `latest_fetched_block_number` - Latest block fetched from source
|
|
- `num_batches_fetched` - Number of batches processed
|
|
|
|
## Basic Queries
|
|
|
|
### Query Entities
|
|
|
|
```bash
|
|
curl -s http://localhost:8080/v1/graphql \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query": "{ Transfer(limit: 10) { id from to amount blockNumber } }"}'
|
|
```
|
|
|
|
### With Ordering
|
|
|
|
```graphql
|
|
{
|
|
Transfer(order_by: { blockNumber: desc }, limit: 10) {
|
|
id
|
|
from
|
|
to
|
|
amount
|
|
}
|
|
}
|
|
```
|
|
|
|
### With Filters
|
|
|
|
```graphql
|
|
{
|
|
Transfer(where: { from: { _eq: "0x123..." } }, limit: 100) {
|
|
id
|
|
from
|
|
to
|
|
amount
|
|
}
|
|
}
|
|
```
|
|
|
|
### Filter by Chain (Multichain)
|
|
|
|
```bash
|
|
curl -s http://localhost:8080/v1/graphql \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query": "{ Transfer(where: {chainId: {_eq: 42161}}, limit: 10) { id chainId from to amount } }"}'
|
|
```
|
|
|
|
## Filter Operators
|
|
|
|
| Operator | Description | Example |
|
|
|----------|-------------|---------|
|
|
| `_eq` | Equals | `{field: {_eq: "value"}}` |
|
|
| `_neq` | Not equals | `{field: {_neq: "value"}}` |
|
|
| `_gt` | Greater than | `{amount: {_gt: "100"}}` |
|
|
| `_gte` | Greater than or equal | `{amount: {_gte: "100"}}` |
|
|
| `_lt` | Less than | `{amount: {_lt: "100"}}` |
|
|
| `_lte` | Less than or equal | `{amount: {_lte: "100"}}` |
|
|
| `_in` | In list | `{chainId: {_in: [1, 10]}}` |
|
|
| `_nin` | Not in list | `{chainId: {_nin: [1]}}` |
|
|
| `_is_null` | Is null | `{field: {_is_null: true}}` |
|
|
| `_like` | Pattern match | `{id: {_like: "1_%"}}` |
|
|
| `_ilike` | Case-insensitive pattern | `{user: {_ilike: "%abc%"}}` |
|
|
|
|
**Important:** BigInt values must be quoted strings: `{amount: {_gt: "1000000000000000000"}}`
|
|
|
|
## Logical Operators
|
|
|
|
```graphql
|
|
# AND - all conditions must match
|
|
where: { _and: [{ chainId: { _eq: 1 } }, { amount: { _gt: "0" } }] }
|
|
|
|
# OR - any condition matches
|
|
where: { _or: [{ from: { _eq: "0x123" } }, { to: { _eq: "0x123" } }] }
|
|
|
|
# NOT - negate condition
|
|
where: { _not: { amount: { _eq: "0" } } }
|
|
```
|
|
|
|
## Pagination
|
|
|
|
### Limit and Offset
|
|
|
|
```graphql
|
|
{
|
|
Transfer(limit: 100, offset: 200) {
|
|
id
|
|
}
|
|
}
|
|
```
|
|
|
|
### Cursor-based (by primary key)
|
|
|
|
```graphql
|
|
{
|
|
Transfer(limit: 100, where: { id: { _gt: "last_seen_id" } }, order_by: { id: asc }) {
|
|
id
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Query Patterns
|
|
|
|
### Recent Transfers for User
|
|
|
|
```graphql
|
|
query UserTransfers($address: String!) {
|
|
Transfer(
|
|
where: {
|
|
_or: [
|
|
{ from: { _eq: $address } },
|
|
{ to: { _eq: $address } }
|
|
]
|
|
}
|
|
order_by: { blockTimestamp: desc }
|
|
limit: 50
|
|
) {
|
|
id
|
|
from
|
|
to
|
|
amount
|
|
blockTimestamp
|
|
}
|
|
}
|
|
```
|
|
|
|
### Polling for Updates
|
|
|
|
```graphql
|
|
query NewTransfers($lastTimestamp: BigInt!) {
|
|
Transfer(where: { blockTimestamp: { _gt: $lastTimestamp } }) {
|
|
id
|
|
from
|
|
to
|
|
amount
|
|
blockTimestamp
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get by Primary Key
|
|
|
|
```bash
|
|
curl -s http://localhost:8080/v1/graphql \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query": "{ Transfer_by_pk(id: \"1_0xabc..._0\") { id from to amount } }"}'
|
|
```
|
|
|
|
## Discovering Schema
|
|
|
|
### List All Query Types
|
|
|
|
```bash
|
|
curl -s http://localhost:8080/v1/graphql \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query": "{ __schema { queryType { fields { name } } } }"}'
|
|
```
|
|
|
|
### Get Entity Fields
|
|
|
|
```bash
|
|
curl -s http://localhost:8080/v1/graphql \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query": "{ __type(name: \"Transfer\") { fields { name type { name } } } }"}'
|
|
```
|
|
|
|
## Aggregations
|
|
|
|
**Local:** Aggregation queries work in Hasura console.
|
|
|
|
**Hosted Service:** Aggregations are **disabled** for performance.
|
|
|
|
**Best Practice:** Compute aggregates at indexing time:
|
|
|
|
```graphql
|
|
# Schema
|
|
type GlobalStats {
|
|
id: ID!
|
|
totalTransfers: Int!
|
|
totalVolume: BigDecimal!
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// Handler - update on each transfer
|
|
const stats = await context.GlobalStats.get("global");
|
|
context.GlobalStats.set({
|
|
...stats,
|
|
totalTransfers: stats.totalTransfers + 1,
|
|
totalVolume: stats.totalVolume.plus(transferAmount),
|
|
});
|
|
```
|
|
|
|
Then query precomputed values:
|
|
```graphql
|
|
{
|
|
GlobalStats(where: { id: { _eq: "global" } }) {
|
|
totalTransfers
|
|
totalVolume
|
|
}
|
|
}
|
|
```
|
|
|
|
## Hasura Console
|
|
|
|
Open `http://localhost:8080` for the visual interface.
|
|
|
|
**API Tab:**
|
|
- Execute GraphQL queries
|
|
- Explorer shows all entities
|
|
- Test queries before frontend integration
|
|
|
|
**Data Tab:**
|
|
- View database tables directly
|
|
- Check `db_write_timestamp` for freshness
|
|
- Manually inspect entities
|
|
|
|
## Fetch from Code
|
|
|
|
```typescript
|
|
const response = await fetch('http://localhost:8080/v1/graphql', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
query: `
|
|
query {
|
|
Transfer(limit: 10) {
|
|
id
|
|
from
|
|
to
|
|
amount
|
|
}
|
|
}
|
|
`
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Check `_meta` first** - Verify indexer progress before assuming data is missing
|
|
2. **Fetch only needed fields** - Reduces response size
|
|
3. **Use pagination** - Never query unlimited results
|
|
4. **Filter on indexed fields** - Use `@index` columns for faster queries
|
|
5. **Poll with timestamps** - Fetch only new data for real-time updates
|
|
6. **Precompute aggregates** - At indexing time, not query time
|
|
7. **BigInt as strings** - Always quote large numbers in filters
|