Custom GraphQL resolvers
One can extend the GraphQL API generated by OpenReader with custom queries. To do that, one can define GraphQL query resolvers in the designated module src/server-extension/resolvers. Note that all resolver classes (including any additional types) must be exported by src/server-extension/resolvers/index.ts. The reflect-metadata package must be imported at the top of that file.
A custom resolver should import TypeGraphQL types and use annotations provided by the library to define query arguments and return types.
Add type-graphql and reflect-metadata packages with:
npm i type-graphql reflect-metadata
Custom resolvers are normally used in combination with TypeORM EntityManager for accessing the API server target database. A callback returning it is automatically injected as a single argument of the resolver class constructor.
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:
- grab a test squid as described here;
- install
type-graphqlandreflect-metadata; - save the example code to
src/server-extension/resolver.ts; - at
src/server-extension/resolvers/index.ts, importreflect-metadataat the very top and re-exportCountResolver:import 'reflect-metadata'
export { CountResolver } from '../resolver' - rebuild the squid with
npm run build; - (re)start the GraphQL server with
npx squid-graphql-server.
totalBurns selection will appear in the GraphiQL playground.
Skipping the reflect-metadata installation and import will leave you with code that may crash, intermittently and sometimes silently.
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 depenency 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-sizeused 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.