Omnihedron

Schema Generation

How omnihedron builds a GraphQL schema from PostgreSQL tables

For every table discovered during introspection, omnihedron generates a complete set of GraphQL types. This happens at runtime using async-graphql's dynamic schema module — no code generation, no compile-time schema.

What gets generated per table

Given a table transfers with columns id, amount, chain, block_number, and a foreign key account_id → accounts.id:

Object type

type Transfer {
  id: String!
  amount: BigInt
  chain: String
  blockNumber: Int
  nodeId: ID!

  # Forward relation (FK → parent record)
  account: Account

  # Backward relation (other tables pointing here)
  # e.g., if "events" has FK "transfer_id → transfers.id"
  eventsByTransferId(first: Int, filter: EventFilter, ...): EventConnection
}

Root queries

type Query {
  # Single record by primary key
  transfer(id: ID!): Transfer

  # Single record by nodeId
  transferByNodeId(nodeId: ID!): Transfer

  # Connection (list with pagination)
  transfers(
    first: Int
    last: Int
    after: Cursor
    before: Cursor
    offset: Int
    orderBy: [TransfersOrderBy!]
    orderByNull: NullOrder
    filter: TransferFilter
    distinct: [TransfersDistinctEnum!]
    blockHeight: String  # only on historical tables
  ): TransferConnection
}

Connection types

type TransferConnection {
  nodes: [Transfer!]!
  edges: [TransferEdge!]!
  totalCount: Int!
  pageInfo: PageInfo!
  aggregates: TransferAggregates  # if --aggregate is enabled
}

type TransferEdge {
  node: Transfer!
  cursor: Cursor!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: Cursor
  endCursor: Cursor
}

Filter input type

Every column gets a set of filter operators based on its type:

input TransferFilter {
  id: StringFilter
  amount: BigIntFilter
  chain: StringFilter
  blockNumber: IntFilter

  # Logical operators
  and: [TransferFilter!]
  or: [TransferFilter!]
  not: TransferFilter

  # Relation filters
  accountExists: Boolean
  account: AccountFilter
  eventsByTransferIdSome: EventFilter
  eventsByTransferIdNone: EventFilter
  eventsByTransferIdEvery: EventFilter
}

String filters include: equalTo, notEqualTo, in, notIn, contains, notContains, startsWith, notStartsWith, endsWith, notEndsWith, like, notLike, ilike, likeInsensitive, notLikeInsensitive, isNull.

Numeric filters include: equalTo, notEqualTo, in, notIn, greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo, isNull.

OrderBy enum

enum TransfersOrderBy {
  ID_ASC
  ID_DESC
  AMOUNT_ASC
  AMOUNT_DESC
  CHAIN_ASC
  CHAIN_DESC
  BLOCK_NUMBER_ASC
  BLOCK_NUMBER_DESC
  # Forward-relation scalar ordering
  ACCOUNT_BY_ACCOUNT_ID__NAME_ASC
  ACCOUNT_BY_ACCOUNT_ID__NAME_DESC
}

Forward-relation ordering generates correlated subqueries in the SQL ORDER BY.

Aggregates

type TransferAggregates {
  count: BigInt!
  sum: TransferSumAggregates
  distinctCount: TransferDistinctCountAggregates
  min: TransferMinAggregates
  max: TransferMaxAggregates
  average: TransferAverageAggregates
  stddevSample: TransferStddevSampleAggregates
  stddevPopulation: TransferStddevPopulationAggregates
  varianceSample: TransferVarianceSampleAggregates
  variancePopulation: TransferVariancePopulationAggregates
}

Only aggregate functions actually requested in the query are computed in SQL.

Special types

Node interface

interface Node {
  nodeId: ID!
}

type Query {
  node(nodeId: ID!): Node
}

The node root query decodes a nodeId, determines the type, and fetches the record.

Metadata

type Query {
  _metadata(chainId: String): _Metadata
  _metadatas(after: Cursor, first: Int): _MetadatasConnection
}

Multi-chain projects store metadata in per-chain tables (_metadata_<genesisHash>). The chainId argument maps to the correct table.

Type mapping

PostgreSQL typeGraphQL scalar
text, varchar, char, name, citextString
int2, int4Int
int8BigInt (JSON string)
numeric, decimalBigFloat (JSON string)
float4, float8Float
boolBoolean
timestamp, timestamptzDatetime (RFC3339)
dateDate (ISO date)
json, jsonbJSON
uuidString
byteaString (hex-encoded)
enum typesCustom scalar with EnumFilter
arraysJSON
unknownString (fallback)

BigInt and BigFloat are serialised as JSON strings to preserve precision — JavaScript Number can't represent large integers without loss.

On this page