A standalone rust service intended to run alongside your hyperindex instance that converts TheGraph subgraph GraphQL queries to Hyperindex/Hasura GraphQL format and forwards them to a Hyperindex endpoint. The tool also converts the responses into the same response format as returned by TheGraph subgraphs. This is useful for automatically porting existing ui's or clients to reading from HyperIndex.
The tool is under active development and is not yet ready for production use. It is currently in a proof-of-concept stage and may not work as expected. It will likely never be a perfect map for all use cases however likely to work for most use cases.
- Query Conversion: Converts subgraph GraphQL syntax to Hyperindex format
- HTTP Forwarding: Forwards converted queries to Hyperindex endpoints
- Environment Configuration: Configurable endpoints & schema's via environment variables
- Error Handling: Comprehensive error handling and logging
- Debug Endpoint: Optional debug endpoint to inspect query conversion
- Chain-Specific Queries: Support for chain-specific queries via
/chainId/{chain_id}endpoint
Converts and forwards queries to Hyperindex without adding any chain-specific filters.
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "query { streams(first: 2, skip: 10) { category cliff cliffTime chainId } }"}' \
http://localhost:3000/Converts and forwards queries to Hyperindex, automatically adding a chainId filter to the where clause.
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "query { streams(first: 2, skip: 10) { category cliff cliffTime chainId } }"}' \
http://localhost:3000/chainId/5This will add where: {chainId: {_eq: "5"}} to the converted query.
Returns the converted query without forwarding to Hyperindex.
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "query { streams(first: 2, skip: 10) { category cliff cliffTime chainId } }"}' \
http://localhost:3000/debug- Plural entity names are singularized and capitalized
- Example:
streams→Stream
| Subgraph Parameter | Hyperindex Parameter | Notes |
|---|---|---|
first |
limit |
Number of records to return |
skip |
offset |
Number of records to skip |
orderBy |
order_by |
Field to sort by (currently unused) |
orderDirection |
order_by direction |
Sort direction (currently unused) |
- Default endpoints (
/and/debug): NochainIdfilter is added - Chain-specific endpoint (
/chainId/{chain_id}): Automatically addswhere: {chainId: {_eq: "{chain_id}"}}to the query - Single Entity by Primary Key: Singular entity queries with only an
idparameter are converted toentity_by_pk(id: ...)format (no chainId filter)
- Selection Sets: Preserved as-is in the converted query
- Single Entity by Primary Key: Singular entity queries with only an
idparameter are converted toentity_by_pk(id: ...)format
The following table shows how TheGraph filter syntax is converted to Hasura equivalents:
| The Graph Filter | Hasura Equivalent | Description | Example (The Graph) | Example (Hasura) |
|---|---|---|---|---|
field |
field: { _eq: val } |
Equal | name: "Alice" |
name: { _eq: "Alice" } |
field_not |
field: { _neq: val } |
Not equal | id_not: "0x123" |
id: { _neq: "0x123" } |
field_gt |
field: { _gt: val } |
Greater than | value_gt: 100 |
value: { _gt: 100 } |
field_gte |
field: { _gte: val } |
Greater than or equal | value_gte: 100 |
value: { _gte: 100 } |
field_lt |
field: { _lt: val } |
Less than | timestamp_lt: 1650000000 |
timestamp: { _lt: 1650000000 } |
field_lte |
field: { _lte: val } |
Less than or equal | timestamp_lte: 1650000000 |
timestamp: { _lte: 1650000000 } |
field_in |
field: { _in: [...] } |
Matches any in array | status_in: ["OPEN", "CLOSED"] |
status: { _in: ["OPEN", "CLOSED"] } |
field_not_in |
field: { _nin: [...] } |
Excludes values in array | id_not_in: ["0x1", "0x2"] |
id: { _nin: ["0x1", "0x2"] } |
field_contains |
field: { _ilike: "%val%" } |
Substring match (case-insensitive) | name_contains: "graph" |
name: { _ilike: "%graph%" } |
field_not_contains |
field: { _not: { _ilike: "%val%" } } |
Substring mismatch | name_not_contains: "graph" |
name: { _not: { _ilike: "%graph%" } } |
field_starts_with |
field: { _ilike: "val%" } |
Starts with | symbol_starts_with: "ETH" |
symbol: { _ilike: "ETH%" } |
field_ends_with |
field: { _ilike: "%val" } |
Ends with | symbol_ends_with: "USD" |
symbol: { _ilike: "%USD" } |
field_not_starts_with |
field: { _not: { _ilike: "val%" } } |
Doesn't start with | name_not_starts_with: "A" |
name: { _not: { _ilike: "A%" } } |
field_not_ends_with |
field: { _not: { _ilike: "%val" } } |
Doesn't end with | name_not_ends_with: "x" |
name: { _not: { _ilike: "%x" } } |
field_contains_nocase |
field: { _ilike: "%val%" } |
Substring match, case-insensitive | name_contains_nocase: "alice" |
name: { _ilike: "%alice%" } |
field_not_contains_nocase |
field: { _not: { _ilike: "%val%" } } |
Substring mismatch, case-insensitive | name_not_contains_nocase: "alice" |
name: { _not: { _ilike: "%alice%" } } |
field_starts_with_nocase |
field: { _ilike: "val%" } |
Case-insensitive prefix match | id_starts_with_nocase: "0xabc" |
id: { _ilike: "0xabc%" } |
field_ends_with_nocase |
field: { _ilike: "%val" } |
Case-insensitive suffix match | id_ends_with_nocase: "def" |
id: { _ilike: "%def" } |
field_not_starts_with_nocase |
field: { _not: { _ilike: "val%" } } |
Case-insensitive negated prefix | name_not_starts_with_nocase: "foo" |
name: { _not: { _ilike: "foo%" } } |
field_not_ends_with_nocase |
field: { _not: { _ilike: "%val" } } |
Case-insensitive negated suffix | name_not_ends_with_nocase: "bar" |
name: { _not: { _ilike: "%bar" } } |
field_containsAny |
❌ No direct equivalent | Array overlap (string[] fields) | tags_containsAny: ["foo", "bar"] |
❌ Requires custom SQL |
field_containsAll |
❌ No direct equivalent | Field contains all values | tags_containsAll: ["foo", "bar"] |
❌ |
id (top-level) |
entity_by_pk(id: ...) |
Get by primary key | user(id: "0x123") |
user_by_pk(id: "0x123") |
- Rust (latest stable version)
- Cargo
- Clone the repository:
git clone <repository-url>
cd subgraph-to-hyperindex-query-converter-poc- Create environment configuration:
cp .env.example .env
# Edit .env with your Hyperindex URL- Build and run:
cargo runThe service will start on http://localhost:3000
Create a .env file in the project root:
HYPERINDEX_URL=https://indexer.hyperindex.xyz/53b7e25/v1/graphqlPOST requests to / will convert and forward queries to Hyperindex without adding chain-specific filters:
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "query { streams(first: 2, skip: 10) { category cliff cliffTime chainId } }"}' \
http://localhost:3000/POST requests to /chainId/{chain_id} will convert and forward queries to Hyperindex, automatically adding a chainId filter:
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "query { streams(first: 2, skip: 10) { category cliff cliffTime chainId } }"}' \
http://localhost:3000/chainId/5POST requests to /debug will return the converted query without forwarding:
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "query { streams(first: 2, skip: 10) { category cliff cliffTime chainId } }"}' \
http://localhost:3000/debugquery {
streams(first: 2, skip: 10) {
category
cliff
cliffTime
chainId
}
}query {
Stream(limit: 2, offset: 10) {
category
cliff
cliffTime
chainId
}
}query {
streams(first: 2, skip: 10) {
category
cliff
cliffTime
chainId
}
}query {
Stream(limit: 2, offset: 10, where: { chainId: { _eq: "5" } }) {
category
cliff
cliffTime
chainId
}
}query {
post(id: "0xabc...") {
title
}
}query {
post_by_pk(id: "0xabc...") {
title
}
}{
"data": {
"Stream": [
{
"category": "LockupDynamic",
"chainId": "1",
"cliff": false,
"cliffTime": null
},
{
"category": "LockupLinear",
"chainId": "1",
"cliff": false,
"cliffTime": null
}
]
}
}- Basic Parsing: Uses simple string parsing instead of a proper GraphQL parser
- Limited Entity Support: Currently optimized for Stream entities
- Order By:
orderByandorderDirectionparameters are extracted but not used in conversion - No Block Queries: Time-traveling queries with
blockparameters are not supported as Hyperindex doesn't natively support historical queries - Data Limit: Unless Hyperindex is configured via environment variables to support 5000 datapoints, the
limitparameter should be set to a maximum of 1000 - _meta Queries: Meta queries are limitted only to latest block number
-
Use proper GraphQL parser for robust query handling
-
Add support for variables and directives
-
Implement proper order_by conversion
-
Add comprehensive test coverage
src/
├── main.rs # HTTP server and routing
└── conversion.rs # Query conversion logic
To add support for new entities or conversion rules, modify the convert_query_structure function in src/conversion.rs.
# Check compilation
cargo check
# Run with debug output
RUST_LOG=debug cargo run
# Test conversion only
cargo testbuild the docker file with a tag docker build -t subgraph-converter .
Create a .env file based on the .env.example file and run the following:
docker run -p 3000:3000 --env-file .env subgraph-converter
test query:
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "query { streams(first: 2, skip: 10) { category cliff cliffTime chainId } }"}' \
http://localhost:3000/
This project is licensed under the MIT License - see the LICENSE file for details.