LakeQL
Introduction
  • Overview
  • Why LakeQL
  • Key Concepts
Getting Started
  • Prerequisites
  • Quickstart
  • Environment
  • First Run
Architecture
  • System Overview
  • Data Flow
  • Request Lifecycle
Configuration
  • Config File
  • Runtime Lifecycle
  • Environment Variables
create-app
  • Usage
  • Template Structure
  • Post-Creation
Guides
  • Custom Resolvers
  • Extending the Schema
  • Mutations
  • Set Up a Trino Mutation
  • Deploying
Endpoints
  • Definition Format
  • Field Types
  • Nested Fields
  • Mutations
  • Glossary
Contributing
  • Local Development
  • Contribution Guide
  • Architecture
GitHub
LakeQL
  1. LakeQL
  2. Guides
  3. Custom Resolvers

Custom Resolvers

Add a computed or aggregate query field to a running project.

The generated endpoints cover table reads and writes. You often need a field that is not a direct table query. An aggregate, a computed value, or a call to another service all fall outside the generated set. Add these as your own query fields on the shared schema builder.

This guide walks through one worked task: add an orderCount aggregate query to a project you already have running.

Create a module for your fields

Put your custom fields in their own file, such as src/custom-fields.ts. Keep them out of the generated files under schemas/generated, because a later pull can overwrite that directory.

1
2
3
// src/custom-fields.ts
import { builder } from "@lakeql/api/builder"

Import the module before you start the server

Add the import to your entry point, before the call to startServer(). The server discovers schema modules by globbing the generated files. It does not import arbitrary files, so a module that is never imported never registers its fields.

1
2
3
4
import "./custom-fields"

await server.startServer()

See the two prerequisites for the globbing rule and the Mutation-type caveat.

Define the field

Add the query field with builder.queryField. Read data through the GraphQL context, which exposes getReadStrategy(name) and getWriteStrategy(name).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/custom-fields.ts
import { builder } from "@lakeql/api/builder"

builder.queryField("orderCount", (t) =>
  t.int({
    resolve: async (_root, _args, context) => {
      const strategy = context.getReadStrategy("warehouse")
      // use the strategy to read, the same path generated resolvers take
      // ...
      return count
    },
  })
)

This is the same builder the generated endpoints use. Your field gets the same auth scopes, input validation, and scalar types.

Require a scope

Declare the scopes the field needs through its authScopes option. Require an authenticated user, or a read permission on a particular table.

1
2
3
4
5
6
7
8
9
10
builder.queryField("orderCount", (t) =>
  t.int({
    authScopes: { authenticated: true },
    resolve: async (_root, _args, context) => {
      // ...
      return count
    },
  })
)

See scope authorization for the scope model.

Validate the arguments

If the field takes arguments, attach a Zod schema through the argument's validate option. The schema runs before the resolver, so the resolver only sees valid input. See input validation.

Run it

Start the dev server and query the new field.

pnpm dev
1
2
3
4
query {
  orderCount
}

For the mutation case and the full reference on each option, see custom queries and mutations.

Previous page

Guides

Next page

Extending the Schema