Skip to main content

Custom GraphQL Resolvers

You can extend the GraphQL API generated by OpenReader with custom queries. To do that, define GraphQL query resolvers in the designated module src/server-extension/resolvers.
All resolver classes (including any additional types) must be exported by src/server-extension/resolvers/index.ts.
A custom resolver should import TypeGraphQL types and use annotations provided by the library to define query arguments and return types. If your squid lacks a type-graphql dependency, add it with:
npm i type-graphql
Custom resolvers are normally used in combination with TypeORM EntityManager for accessing the API server target database. It is automatically injected when defined as a single constructor argument of the resolver.

Examples

Simple entity counter

import { Query, Resolver } from "type-graphql";
import type { EntityManager } from "typeorm";
import { Burn } from "../model";

@Resolver()
export class CountResolver {
  constructor(private tx: () => Promise<EntityManager>) {}

  @Query(() => Number)
  async totalBurns(): Promise<number> {
    const manager = await this.tx();
    return await manager.getRepository(Burn).count();
  }
}
This example is designed to work with the evm template:
1

Grab a test squid

Follow the instructions in squid development guide.
2

Install type-graphql

npm i type-graphql
3

Save the example code

Save the example code to src/server-extension/resolver.ts.
4

Re-export CountResolver

Re-export CountResolver at src/server-extension/resolvers/index.ts:
export { CountResolver } from "../resolver";
5

Rebuild and restart

Rebuild the squid with npm run build and (re)start the GraphQL server with npx squid-graphql-server.
totalBurns selection will appear in the GraphiQL playground.

Custom SQL query

import { Arg, Field, ObjectType, Query, Resolver } from "type-graphql";
import type { EntityManager } from "typeorm";
import { MyEntity } from "../model";

// Define custom GraphQL ObjectType of the query result
@ObjectType()
export class MyQueryResult {
  @Field(() => Number, { nullable: false })
  total!: number;

  @Field(() => Number, { nullable: false })
  max!: number;

  constructor(props: Partial<MyQueryResult>) {
    Object.assign(this, props);
  }
}

@Resolver()
export class MyResolver {
  // Set by dependency injection
  constructor(private tx: () => Promise<EntityManager>) {}

  @Query(() => [MyQueryResult])
  async myQuery(): Promise<MyQueryResult[]> {
    const manager = await this.tx();
    // execute custom SQL query
    const result = await manager.getRepository(MyEntity).query(`
      SELECT 
        COUNT(x) as total, 
        MAX(y) as max
      FROM my_entity 
      GROUP BY month
    `);
    return result;
  }
}

More examples

Some great examples of @subsquid/graphql-server-based custom resolvers can be spotted in the wild in the Rubick repo by KodaDot. For more examples of resolvers, see TypeGraphQL examples repo.

Logging

To keep logging consistent across the entire GraphQL server, use @subsquid/logger:
import { createLogger } from "@subsquid/logger";

// using a custom namespace ':my-resolver' for resolver logs
const LOG = createLogger("sqd:graphql-server:my-resolver");
LOG.info("created a dedicated logger for my-resolver");
LOG here is a logger object identical to ctx.log interface-wise.

Interaction with global settings

  • --max-response-size used for DoS protection is ignored in custom resolvers.
  • Caching works on custom queries in exactly the same way as it does on the schema-derived queries.

Troubleshooting

Reflect.getMetadata is not a function

Add import 'reflect-metadata' on top of your custom resolver module and install the package if necessary.