docId: api

source: api

slug: api

title: API

documentType: unknown

contentOrigin: static-doc

content:
---
title: API
navTitle: API
description: GraphQL API server with Hono, Yoga, and Pothos.
entrypoint: /docs/api/overview/introduction
---

navTitle: API

keywords: graphql server pothos


docId: api/authentication/jwt-authentication

slug: authentication/jwt-authentication

title: JWT Authentication

text: GetUserResolver type

text: Default behavior

text: Mock authentication

text: Custom JWT verification

content:
---
title: JWT Authentication
navTitle: JWT Authentication
description: Configure user authentication via JWT tokens or mock authentication for development.
---

LakeQL uses JWT-based authentication to identify the current user on each request. The `getUser` resolver extracts user information from the `Authorization` header and makes it available in the GraphQL context.

## GetUserResolver type

```ts
type GetUserResolver = (
  req: Request
) => Promise<JWTPayload | null | undefined> | JWTPayload | null | undefined
```

Returning `null` or `undefined` means the request is unauthenticated. The `JWTPayload` type extends jose's `JWTPayload` with an additional `userName` field:

```ts
// Extends jose JWTPayload
interface JWTPayload {
  userName: string
  // ...standard JWT claims (iss, sub, aud, exp, etc.)
}
```

## Default behavior

The built-in `getUser` resolver supports mock authentication for local development. It does not perform real JWT verification — you must provide a custom resolver for production.

## Mock authentication

Set the following environment variables to enable mock auth:

```bash
AUTH_MOCK=true
AUTH_MOCK_TOKEN=my-dev-token
```

Then pass the token as the `Authorization` header and the username via `x-username`:

```bash
curl -X POST http://localhost:4000/graphql \
  -H "Authorization: my-dev-token" \
  -H "x-username: developer" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __typename }"}'
```

## Custom JWT verification

For production, provide a custom `getUser` resolver that verifies tokens using your identity provider:

```ts path="src/auth.ts"
import type { GetUserResolver } from "@lakeql/api/types"
import { jwtVerify, createRemoteJWKSet } from "jose"

const jwks = createRemoteJWKSet(
  new URL("https://auth.example.com/.well-known/jwks.json")
)

export const getUser: GetUserResolver = async (req) => {
  const authHeader = req.headers.get("authorization")
  if (!authHeader?.startsWith("Bearer ")) {
    return null
  }

  const token = authHeader.slice(7)

  try {
    const { payload } = await jwtVerify(token, jwks, {
      issuer: "https://auth.example.com",
      audience: "lakeql-api",
    })

    return {
      ...payload,
      userName: payload.sub ?? "unknown",
    }
  } catch {
    return null
  }
}
```

Pass it to `defineConfig` in your `src/config.ts`:

```ts path="src/config.ts"
import { defineConfig } from "@lakeql/api/config"

import { getUser } from "./auth"
import { allConfigs } from "./config-registry"

export const config = defineConfig({
  allConfigs,
  baseDir: import.meta.dirname,
  getUser,
  schemaPath: "./schemas",
})
```

The resolved user is available in every GraphQL resolver via `context.currentUser`.

navTitle: JWT Authentication

keywords: authentication configure tokens development getuserresolver


docId: api/authentication/permissions

slug: authentication/permissions

title: Permissions

text: Permission interface

text: Wildcard access

text: createPermission helper

text: How permissions are evaluated

content:
---
title: Permissions
description: Define table-level permission rules for technical users via the Permission interface.
---

Permissions define which catalogs, schemas, and tables a technical user can access. They act as an application-level allow list on top of Trino's built-in authorization — primarily useful for service accounts that execute queries via a shared system user.

## Permission interface

<InterfaceReference file="api/src/types" name="Permission" />

## Wildcard access

Use `["*"]` in the `tables` array to grant access to all tables within a catalog/schema combination:

```ts
{
  catalog: "hive",
  schema: "analytics",
  tables: ["*"] // access all tables in hive.analytics
}
```

## createPermission helper

For type-safe permission construction that validates against your generated configs, use the `createPermission` helper with `defineConfig`:

```ts path="src/permissions.ts"
import { createPermission as createPermissionFromApi } from "@lakeql/api/helpers"
import type { Permission } from "@lakeql/api/types"

import { allConfigs } from "./config-registry"

const createPermission = createPermissionFromApi(allConfigs)

export const permissions: Permission[] = [
  {
    name: "data-pipeline-service",
    useSystemUser: true,
    permissions: {
      Query: [
        createPermission("hive", "raw_data", ["events", "users", "sessions"]),
      ],
      Mutation: [createPermission("hive", "processed", ["aggregated_events"])],
    },
  },
  {
    name: "reporting-service",
    useSystemUser: true,
    permissions: {
      Query: [createPermission("hive", "analytics", ["*"])],
      Mutation: [],
    },
  },
]
```

## How permissions are evaluated

- **Read (Query):** If no permission entry exists for a user, reads are allowed (Trino handles auth for human users). If rules exist, at least one must match the requested catalog/schema/table.
- **Write (Mutation):** If no permission entry exists, writes are denied. Rules must explicitly grant access.

See [Scope Authorization](/docs/api/authentication/scope-authorization) for the full evaluation logic.

keywords: permission permissions interface define table-level


docId: api/authentication/scope-authorization

slug: authentication/scope-authorization

title: Scope Authorization

text: Auth scopes

text: Read permission logic

text: Write permission logic

text: Custom resolvers

text: Applying scopes to fields

content:
---
title: Scope Authorization
description: Three authorization scopes control access to GraphQL fields — authorized, readPermission, and writePermission.
---

LakeQL uses Pothos's scope auth plugin to enforce authorization at the field level. Every query or mutation field can declare which scope is required to access it.

## Auth scopes

| Scope             | Check                                              | Description                            |
| ----------------- | -------------------------------------------------- | -------------------------------------- |
| `authorized`      | `!!context.currentUser`                            | User is authenticated (any valid JWT)  |
| `readPermission`  | Evaluates catalog/schema/table against permissions | User can read from the specified table |
| `writePermission` | Evaluates catalog/schema/table against permissions | User can write to the specified table  |

## Read permission logic

The default `hasReadPermission` resolver follows this decision model:

1. **No user** → deny
2. **No permission entry for user** → allow (Trino handles auth for human users)
3. **Permission entry exists but no Query rules** → allow
4. **Query rules exist** → at least one rule must match the catalog, schema, and table name

This default-allow model works because human users (OAuth2 Authorization Code Flow) typically have direct Trino identities. Technical users that go through a shared system account need explicit rules.

## Write permission logic

The default `hasWritePermission` resolver is stricter:

1. **No user** → deny
2. **No permission entry for user** → deny
3. **Permission entry exists but no Mutation rules** → deny
4. **Mutation rules exist** → at least one rule must match the catalog, schema, and table name

Writes always require explicit permission because they execute via a system user in Trino and bypass Trino's per-user authorization.

## Custom resolvers

Override the default logic by providing custom resolvers in `defineConfig`:

```ts path="src/config.ts"
import { defineConfig } from "@lakeql/api/config"

import { allConfigs } from "./config-registry"

export const config = defineConfig({
  allConfigs,
  hasReadPermission: ({ context, catalog, schema, tableName }) => {
    // Custom logic: check external authorization service
    return checkExternalAuthService(context.currentUser, {
      action: "read",
      resource: `${catalog}.${schema}.${tableName}`,
    })
  },
  hasWritePermission: ({ context, catalog, schema, tableName }) => {
    // Custom logic: all writes require admin role
    return context.currentUser?.role === "admin"
  },
})
```

## Applying scopes to fields

When building custom query schemas, use `authScopes` to protect fields:

```ts
import { builder } from "@lakeql/api/builder"

builder.queryField("sensitiveData", (t) =>
  t.field({
    type: "String",
    authScopes: {
      readPermission: {
        catalog: "hive",
        schema: "internal",
        tableName: "secrets",
      },
    },
    resolve: () => "classified information",
  })
)
```

For mutations, use `writePermission`:

```ts
builder.mutationField("updateRecord", (t) =>
  t.field({
    type: "Boolean",
    authScopes: {
      writePermission: {
        catalog: "hive",
        schema: "production",
        tableName: "records",
      },
    },
    args: {
      id: t.arg.string({ required: true }),
      value: t.arg.string({ required: true }),
    },
    resolve: async (_parent, args, context) => {
      // perform the write operation
      return true
    },
  })
)
```

Fields without `authScopes` are publicly accessible (no authentication required).

keywords: scopes authorization fields permission logic


docId: api/customization/cors-configuration

slug: customization/cors-configuration

title: CORS Configuration

text: Default CORS configuration

text: Customizing CORS

text: Adding custom middleware

text: Adding authentication middleware

content:
---
title: CORS Configuration
navTitle: CORS Configuration
description: Customize CORS settings and add additional Hono middleware to the API server.
---

By default, `createApiServer` applies permissive CORS settings on the GraphQL endpoint. For production deployments, you'll typically want to restrict the allowed origins and customize headers.

## Default CORS configuration

The built-in CORS middleware is applied to POST, GET, and OPTIONS requests on the GraphQL path:

| Setting        | Default Value                       |
| -------------- | ----------------------------------- |
| `origin`       | `"*"` (all origins)                 |
| `allowMethods` | `["POST", "GET", "OPTIONS"]`        |
| `allowHeaders` | `["content-type", "authorization"]` |
| `credentials`  | `true`                              |

## Customizing CORS

Since `createApiServer` returns the Hono `app` instance, you can add or override middleware:

```ts
import { createApiServer } from "@lakeql/api/server"
import { cors } from "hono/cors"

const { app } = await createApiServer({
  schemaPath: "./src/schemas",
})

// Add restrictive CORS for a specific path
app.use(
  "/graphql/*",
  cors({
    origin: ["https://app.example.com", "https://admin.example.com"],
    allowMethods: ["POST", "OPTIONS"],
    allowHeaders: ["content-type", "authorization", "x-request-id"],
    credentials: true,
    maxAge: 86400,
  })
)
```

## Adding custom middleware

The Hono app supports any compatible middleware. Add rate limiting, request logging, or custom headers:

```ts
import { createApiServer, startApiServer } from "@lakeql/api/server"
import { serve } from "@hono/node-server"

const { app, yoga } = await createApiServer({
  schemaPath: "./src/schemas",
})

// Custom request ID header
app.use("*", async (c, next) => {
  const requestId = crypto.randomUUID()
  c.header("x-request-id", requestId)
  await next()
})

// Custom health endpoint
app.get("/ready", (c) =>
  c.json({ status: "ready", timestamp: new Date().toISOString() })
)

// Start manually with the customized app
serve({
  fetch: app.fetch,
  port: 4000,
})
```

## Adding authentication middleware

For scenarios where you need pre-GraphQL authentication checks:

```ts
import { createApiServer } from "@lakeql/api/server"

const { app } = await createApiServer({
  schemaPath: "./src/schemas",
})

// Block unauthenticated requests before they reach Yoga
app.use("/graphql/*", async (c, next) => {
  const auth = c.req.header("authorization")
  if (!auth && c.req.method !== "OPTIONS") {
    return c.json({ error: "Authorization required" }, 401)
  }
  await next()
})
```

Note that this is separate from the GraphQL-level `authScopes` — it blocks requests at the HTTP layer before they reach the GraphQL resolver.

navTitle: CORS Configuration

keywords: middleware configuration adding customize settings


slug: customization/custom-queries-mutations

title: Custom Queries & Mutations

text: Creating an endpoint definition

text: Generating the endpoint

text: Query-only endpoints

text: Field options

text: Mutation load strategies

text: File discovery

content:
---
title: Custom Queries & Mutations
navTitle: Custom Queries & Mutations
description: Add custom query and mutation endpoints using the CLI endpoint builder.
---

The recommended way to add custom queries and mutations is through the CLI's `create-endpoint` command. It generates all necessary files (schema, config, types, JSON Schema) from a simple endpoint definition — the same way the `pull` command works, but from a JSON definition instead of an existing Trino table.

## Creating an endpoint definition

Define your endpoint as a JSON file:

```json path="my-endpoint.json"
{
  "version": "1.0",
  "tableName": "user_events",
  "catalog": "hive",
  "schema": "analytics",
  "fields": [
    { "name": "event_id", "type": "String", "options": { "required": true } },
    { "name": "message", "type": "String" },
    { "name": "timestamp", "type": "DateTime" }
  ],
  "mutation": {
    "loadStrategy": "full_load",
    "type": "minio",
    "bucket": "my-datalake",
    "basePath": "analytics/user_events",
    "endpoint": "http://localhost:9000"
  }
}
```

## Generating the endpoint

Run the CLI to generate all files:

<Command variant="exec">
  lakeql-cli create-endpoint --from-file ./my-endpoint.json
</Command>

This generates:

```
src/schemas/custom/hive/analytics/user_events/
├── config.ts              # Hive + storage config
├── endpoint.json          # Persisted definition
├── json-schema.json       # JSON Schema for Parquet serialization
├── query-schema.ts        # GraphQL query with sorting, filtering, paging
└── mutation-schema.ts     # GraphQL mutation with write pipeline
```

The `config-registry.ts` is updated automatically to include the new endpoint.

## Query-only endpoints

Omit the `mutation` field to generate a query-only endpoint:

```json path="query-only.json"
{
  "version": "1.0",
  "tableName": "reports",
  "catalog": "hive",
  "schema": "analytics",
  "fields": [
    { "name": "report_id", "type": "String" },
    { "name": "title", "type": "String" },
    { "name": "created_at", "type": "DateTime" }
  ]
}
```

Or explicitly disable mutations:

```json
{ "mutation": false }
```

## Field options

Each field supports options for mutation input behavior:

| Option        | Default | Description                                                    |
| ------------- | ------- | -------------------------------------------------------------- |
| `required`    | `false` | Field is required in mutation input                            |
| `readOnly`    | `false` | Field appears in queries but is excluded from mutation input   |
| `validations` | `[]`    | Zod validation refinements (email, url, uuid, min, max, regex) |

```json
{
  "name": "email",
  "type": "String",
  "options": {
    "required": true,
    "validations": [{ "type": "email" }]
  }
}
```

## Mutation load strategies

The `mutation` config supports three strategies. See [Load Strategies](/docs/adapters/write-pipeline/load-strategies) for details.

| Strategy           | Behavior                        |
| ------------------ | ------------------------------- |
| `full_load`        | Replace all data on every write |
| `full_load_append` | Replace latest + keep history   |
| `append`           | Append only                     |

## File discovery

The API server automatically discovers all schema files at startup by scanning for:

```
schemas/**/query-schema.{ts,js,mjs}
schemas/**/mutation-schema.{ts,js,mjs}
```

No manual registration is needed — just generate and start the server.

navTitle: Custom Queries & Mutations

keywords: endpoint custom mutation endpoints queries


docId: api/customization/extending-core

slug: customization/extending-core

title: Extending Core

text: Custom GetUserResolver

text: Custom ReadPermissionResolver

text: Custom WritePermissionResolver

text: Registering custom resolvers

text: Adding custom scalars

content:
---
title: Extending Core
description: Provide custom resolvers for authentication, permissions, and add additional scalars.
---

LakeQL's extension points let you replace built-in behavior with custom implementations. The primary customization targets are the authentication resolver, permission resolvers, and scalar types.

## Custom GetUserResolver

Replace the built-in mock auth with your own JWT verification:

```ts
import type { GetUserResolver } from "@lakeql/api/types"
import { jwtVerify } from "jose"

export const customGetUser: GetUserResolver = async (req) => {
  const token = req.headers.get("authorization")?.replace("Bearer ", "")
  if (!token) return null

  try {
    const { payload } = await jwtVerify(token, secretKey)
    return { ...payload, userName: payload.sub ?? "unknown" }
  } catch {
    return null
  }
}
```

## Custom ReadPermissionResolver

Override how read permissions are evaluated:

```ts
import type { ReadPermissionResolver } from "@lakeql/api/types"

export const customReadPermission: ReadPermissionResolver = ({
  context,
  catalog,
  schema,
  tableName,
}) => {
  // Allow all reads for admin users
  if (context.currentUser?.role === "admin") {
    return true
  }

  // Check against external policy engine
  return checkPolicy(context.currentUser, "read", {
    catalog,
    schema,
    tableName,
  })
}
```

## Custom WritePermissionResolver

Override how write permissions are evaluated:

```ts
import type { WritePermissionResolver } from "@lakeql/api/types"

export const customWritePermission: WritePermissionResolver = ({
  context,
  catalog,
  schema,
  tableName,
}) => {
  // Only service accounts can write
  if (!context.currentUser?.isServiceAccount) {
    return false
  }

  return checkPolicy(context.currentUser, "write", {
    catalog,
    schema,
    tableName,
  })
}
```

## Registering custom resolvers

Pass all custom resolvers via `defineConfig`:

```ts
import { defineConfig } from "@lakeql/api/config"
import { allConfigs } from "./generated/configs"
import { customGetUser } from "./auth"
import { customReadPermission, customWritePermission } from "./permissions"

export default defineConfig({
  allConfigs,
  getUser: customGetUser,
  hasReadPermission: customReadPermission,
  hasWritePermission: customWritePermission,
})
```

## Adding custom scalars

Register additional scalars on the shared builder in a `query-schema.ts` file. Place it in a directory that sorts alphabetically before your other schemas (e.g., `00-scalars/`) to ensure the scalars are available when other schema files are loaded.

```ts path="src/schemas/00-scalars/query-schema.ts"
import { builder } from "@lakeql/api/builder"
import { JSONResolver, BigIntResolver } from "graphql-scalars"

builder.addScalarType("JSON", JSONResolver, {})
builder.addScalarType("BigInt", BigIntResolver, {})
```

<Callout type="info">
  `graphql` and `graphql-scalars` are transitive dependencies of `@lakeql/api`.
  To avoid issues if those internals ever change, install them explicitly in
  your project.

<Command variant="install">graphql graphql-scalars</Command>

</Callout>

These scalars become available in all other schema files loaded after this one. Place scalar definitions in a directory that sorts alphabetically before other schemas (e.g., `00-scalars/`) to ensure they're loaded first.

## Overriding built-in scalar serialization

GraphQL's built-in `Int` scalar only accepts 32-bit signed integers (max ~2.1 billion). Both Hive/Trino `INT` and `BIGINT` columns are mapped to the `Integer` field type by the `pull` command, so values beyond the 32-bit range can reach GraphQL at runtime. When that happens, the default `Int.serialize` throws a range error.

You can monkey-patch the serializer at startup to handle these edge cases.

### Setup

Create a `query-schema.ts` file in a directory that sorts alphabetically before your other schemas (e.g., `00-scalars/`). LakeQL loads all `query-schema.{ts,js,mjs}` and `mutation-schema.{ts,js,mjs}` files in alphabetical directory order, so the override will be active before any resolvers execute.

```text
src/
└── schemas/
    ├── 00-scalars/
    │   └── query-schema.ts   ← Int override goes here
    ├── my-catalog/
    │   └── ...
    └── ...
```

```ts path="src/schemas/00-scalars/query-schema.ts"
import { GraphQLInt } from "graphql"

// Preserve the original serializer as a fallback
const originalIntSerialize = GraphQLInt.serialize.bind(GraphQLInt)

GraphQLInt.serialize = (value: unknown) => {
  // Pass through any safe integer value directly, even beyond 32-bit range
  if (
    typeof value === "number" &&
    Number.isInteger(value) &&
    Number.isFinite(value)
  ) {
    return value
  }

  // Handle numeric strings (e.g., from database drivers returning string IDs)
  if (typeof value === "string" && value !== "") {
    const num = Number(value)
    if (Number.isInteger(num) && Number.isFinite(num)) return num
  }

  // Fall back to default behavior for anything else
  return originalIntSerialize(value)
}
```

Since this file is named `query-schema.ts` and lives in `00-scalars/`, LakeQL will import it automatically — no additional configuration or explicit import is needed.

keywords: custom resolvers scalars extending provide


docId: api/overview/introduction

slug: overview/introduction

title: Introduction

text: How it connects

text: Schema loading

text: Quick start

content:
---
title: Introduction
description: What @lakeql/api provides and how it fits into the LakeQL ecosystem.
---

`@lakeql/api` is the runtime GraphQL server package in the LakeQL ecosystem. It exposes a fully typed GraphQL API that queries data from Trino — a distributed SQL query engine — and returns structured, paginated results to clients.

The package combines three core libraries into a cohesive server stack:

- **Hono** — A lightweight, high-performance HTTP framework that handles routing, CORS, and middleware.
- **GraphQL Yoga** — A spec-compliant GraphQL server with built-in support for subscriptions, file uploads, and health checks.
- **Pothos** — A code-first, type-safe GraphQL schema builder that generates the schema from TypeScript definitions.

## How it connects

`@lakeql/api` sits at the center of the LakeQL architecture. It depends on several sibling packages:

| Package                        | Role                                                          |
| ------------------------------ | ------------------------------------------------------------- |
| `@lakeql/trino-client`         | Executes SQL queries against Trino                            |
| `@lakeql/query-builder`        | Constructs SQL from GraphQL arguments (filters, pagination)   |
| `@lakeql/response-transformer` | Transforms raw Trino responses into GraphQL-compatible shapes |
| `@lakeql/helpers`              | Shared utility functions                                      |
| `@lakeql/logger`               | Structured logging                                            |

## Schema loading

Query and mutation schemas are loaded automatically at startup using glob patterns. The server scans for files matching `schemas/**/{query,mutation}-schema.{ts,js,mjs}` in the configured schema directory. Each schema file registers its types and resolvers on the shared Pothos builder — no manual wiring required.

## Quick start

Install the required packages:

<Command variant="install">
  @lakeql/api @lakeql/query-builder @lakeql/trino-client @t3-oss/env-core zod
</Command>

Set up the configuration (`src/config.ts`):

```ts path="src/config.ts"
import { defineConfig } from "@lakeql/api/config"

import { getUser } from "./auth"
import { allConfigs } from "./config-registry"
import { permissions } from "./permissions"

const baseDir = import.meta.dirname

export const config = defineConfig({
  allConfigs,
  baseDir,
  getUser,
  graphqlPath: "/graphql",
  healthCheckEndpoint: "/live",
  permissions,
  port: 4000,
  schemaPath: "./schemas",
})
```

Create the entry point (`src/index.ts`):

```ts path="src/index.ts"
import { config } from "./config"

await config.startServer()
```

The server starts on `http://localhost:4000` with GraphiQL available at `/graphql` in development mode.

<Note title="Use @lakeql/create-app for a ready-to-go setup">
  Instead of wiring everything manually, use
  [`@lakeql/create-app`](/docs/lakeql/create-app/usage) to scaffold a fully
  functional project with auth, permissions, environment validation, and CLI
  integration pre-configured.
</Note>

keywords: introduction lakeqlapi provides lakeql ecosystem


slug: schema-builder/builder-configuration

title: Builder Configuration

text: Builder setup

text: Context type

text: Auth scopes

text: SortDirection enum

text: Importing the builder

content:
---
title: Builder Configuration
description: Pothos SchemaBuilder setup with scope auth, validation plugins, and the GraphQL context type.
---

The Pothos `SchemaBuilder` is the foundation of LakeQL's type-safe GraphQL schema. It's pre-configured with auth scopes and validation, and shared across all query schema files.

## Builder setup

```ts
import SchemaBuilder from "@pothos/core"
import ScopeAuthPlugin from "@pothos/plugin-scope-auth"
import ValidationPlugin from "@pothos/plugin-validation"

const builder = new SchemaBuilder<{
  Context: Context
  Scalars: Partial<UserScalars["Scalars"]>
  AuthScopes: {
    authorized: boolean
    readPermission: PermissionFields
    writePermission: PermissionFields
  }
}>({
  plugins: [ScopeAuthPlugin, ValidationPlugin],
  scopeAuth: {
    /* ... */
  },
  validation: {
    /* ... */
  },
})
```

## Context type

Every resolver receives this context object:

<InterfaceReference file="api/src/types" name="Context" />

A `logger` instance is also injected into the context by the Yoga server setup.

## Auth scopes

The builder defines three scopes that can be applied to any field:

```ts
AuthScopes: {
  authorized: boolean // user is authenticated
  readPermission: PermissionFields // { catalog, schema, tableName }
  writePermission: PermissionFields // { catalog, schema, tableName }
}
```

## SortDirection enum

A shared enum for ordering query results:

```ts
import { SortDirection } from "@lakeql/api/builder"
// Values: "ASC" | "DESC"
```

## Importing the builder

Custom query schemas import the shared builder instance to register types and fields:

```ts
import { builder } from "@lakeql/api/builder"

builder.queryField("hello", (t) =>
  t.string({
    resolve: () => "world",
  })
)
```

All files that import `builder` and define types or fields are automatically discovered during schema loading — no explicit registration needed.

keywords: builder setup context configuration pothos


docId: api/schema-builder/comparison-types

slug: schema-builder/comparison-types

title: Comparison Types

text: Available comparison types

text: Usage in GraphQL queries

text: Importing comparison types

content:
---
title: Comparison Types
description: Pre-built GraphQL input types for filtering queries with typed comparison operators.
---

LakeQL provides seven comparison input types that power the generated `Where` filter inputs. Each type exposes operators appropriate for its scalar type.

## Available comparison types

### StringFieldComparison

| Operator  | Type       | Description                |
| --------- | ---------- | -------------------------- |
| `eq`      | `String`   | Equal to                   |
| `neq`     | `String`   | Not equal to               |
| `like`    | `String`   | SQL LIKE pattern match     |
| `notLike` | `String`   | SQL NOT LIKE pattern match |
| `in`      | `[String]` | Value in list              |
| `notIn`   | `[String]` | Value not in list          |

### IntFieldComparison

| Operator | Type    | Description           |
| -------- | ------- | --------------------- |
| `eq`     | `Int`   | Equal to              |
| `neq`    | `Int`   | Not equal to          |
| `lt`     | `Int`   | Less than             |
| `lte`    | `Int`   | Less than or equal    |
| `gt`     | `Int`   | Greater than          |
| `gte`    | `Int`   | Greater than or equal |
| `in`     | `[Int]` | Value in list         |
| `notIn`  | `[Int]` | Value not in list     |

### FloatFieldComparison

| Operator | Type      | Description           |
| -------- | --------- | --------------------- |
| `eq`     | `Float`   | Equal to              |
| `neq`    | `Float`   | Not equal to          |
| `lt`     | `Float`   | Less than             |
| `lte`    | `Float`   | Less than or equal    |
| `gt`     | `Float`   | Greater than          |
| `gte`    | `Float`   | Greater than or equal |
| `in`     | `[Float]` | Value in list         |
| `notIn`  | `[Float]` | Value not in list     |

### BooleanFieldComparison

| Operator | Type      | Description |
| -------- | --------- | ----------- |
| `is`     | `Boolean` | Is true     |
| `isNot`  | `Boolean` | Is false    |

### DateFieldComparison

| Operator | Type   | Description           |
| -------- | ------ | --------------------- |
| `eq`     | `Date` | Equal to              |
| `neq`    | `Date` | Not equal to          |
| `lt`     | `Date` | Less than             |
| `lte`    | `Date` | Less than or equal    |
| `gt`     | `Date` | Greater than          |
| `gte`    | `Date` | Greater than or equal |

### DateTimeFieldComparison

| Operator | Type       | Description           |
| -------- | ---------- | --------------------- |
| `eq`     | `DateTime` | Equal to              |
| `neq`    | `DateTime` | Not equal to          |
| `lt`     | `DateTime` | Less than             |
| `lte`    | `DateTime` | Less than or equal    |
| `gt`     | `DateTime` | Greater than          |
| `gte`    | `DateTime` | Greater than or equal |

### IDFieldComparison

| Operator | Type   | Description       |
| -------- | ------ | ----------------- |
| `eq`     | `ID`   | Equal to          |
| `neq`    | `ID`   | Not equal to      |
| `in`     | `[ID]` | Value in list     |
| `notIn`  | `[ID]` | Value not in list |

## Usage in GraphQL queries

These comparison types appear in generated `Where` input types for each table. Use them to filter query results:

```graphql
query FilteredUsers {
  users(
    where: {
      age: { gte: 18, lt: 65 }
      email: { like: "%@example.com" }
      status: { in: ["active", "pending"] }
    }
  ) {
    nodes {
      id
      name
      email
    }
  }
}
```

## Importing comparison types

If you need to reference these types in custom schemas:

```ts
import {
  StringFieldComparison,
  IntFieldComparison,
  FloatFieldComparison,
  BooleanFieldComparison,
  DateFieldComparison,
  DateTimeFieldComparison,
  IDFieldComparison,
} from "@lakeql/api/builder"
```

keywords: comparison types graphql queries pre-built


docId: api/schema-builder/input-validation

slug: schema-builder/input-validation

title: Input Validation

text: How it works

text: Error response format

text: Built-in validation

text: Adding validation to custom inputs

text: Validation on query arguments

content:
---
title: Input Validation
description: Validate GraphQL input fields using Zod schemas via the Pothos ValidationPlugin.
---

LakeQL integrates Pothos's ValidationPlugin with Zod to validate input arguments before they reach your resolvers. Invalid inputs return a structured `VALIDATION_FAILED` error with detailed issues.

## How it works

The ValidationPlugin is registered on the shared builder and uses Zod schemas to validate input field values. When validation fails, a custom error handler formats the response with error code `200` (VALIDATION_FAILED) and the list of Zod issues.

## Error response format

When validation fails, the GraphQL error includes:

```json
{
  "errors": [
    {
      "message": "Validation failed",
      "extensions": {
        "code": "VALIDATION_FAILED",
        "http": { "status": 400 },
        "additionalInformation": [
          {
            "message": "Value must be less than or equal to 2000",
            "path": ["perPage"]
          }
        ]
      }
    }
  ]
}
```

## Built-in validation

The `Paging` input type uses validation to enforce `perPage` limits:

```ts
import { z } from "zod"
import { builder, getMaxRecordsPerPage } from "@lakeql/api/builder"

const Paging = builder.inputType("Paging", {
  fields: (t) => ({
    page: t.int({ defaultValue: 1 }),
    perPage: t.int({
      defaultValue: 100,
      validate: z
        .number()
        .min(1)
        .refine((value) => value <= getMaxRecordsPerPage(), {
          message: `Value must be less than or equal to ${getMaxRecordsPerPage()}`,
        }),
    }),
  }),
})
```

## Adding validation to custom inputs

Use the `validate` option on any input field to attach a Zod schema:

```ts
import { z } from "zod"
import { builder } from "@lakeql/api/builder"

const CreateUserInput = builder.inputType("CreateUserInput", {
  fields: (t) => ({
    email: t.string({
      required: true,
      validate: z.string().email("Must be a valid email address"),
    }),
    name: t.string({
      required: true,
      validate: z.string().min(2).max(100),
    }),
    age: t.int({
      validate: z.number().min(0).max(150),
    }),
  }),
})
```

## Validation on query arguments

You can also validate query-level arguments:

```ts
import { z } from "zod"
import { builder } from "@lakeql/api/builder"

builder.queryField("search", (t) =>
  t.field({
    type: ["String"],
    args: {
      query: t.arg.string({
        required: true,
        validate: z
          .string()
          .min(3, "Search query must be at least 3 characters"),
      }),
      limit: t.arg.int({
        defaultValue: 10,
        validate: z.number().min(1).max(100),
      }),
    },
    resolve: (_parent, args) => {
      // args.query is guaranteed to be at least 3 chars
      // args.limit is guaranteed to be between 1 and 100
      return [`Result for: ${args.query}`]
    },
  })
)
```

keywords: validation input validate graphql fields


docId: api/schema-builder/pagination

slug: schema-builder/pagination

title: Pagination

text: Paging input

text: PageInfo output

text: ConnectionInterface

text: Configuring maxRecordsPerPage

text: Example query

content:
---
title: Pagination
description: Built-in page-based pagination with Paging input, PageInfo output, and ConnectionInterface.
---

LakeQL uses offset-based pagination (page + perPage) rather than cursor-based pagination. This maps naturally to Trino's `LIMIT` and `OFFSET` clauses and provides predictable navigation for tabular data.

## Paging input

The `Paging` input type is available on all generated query fields:

```graphql
input Paging {
  page: Int = 1
  perPage: Int = 100
}
```

| Field     | Default | Constraints                         |
| --------- | ------- | ----------------------------------- |
| `page`    | `1`     | Must be ≥ 1                         |
| `perPage` | `100`   | Must be ≥ 1 and ≤ maxRecordsPerPage |

## PageInfo output

Every paginated response includes a `PageInfo` object:

```graphql
type PageInfo {
  currentPage: Int!
  hasNext: Boolean!
  hasPrevious: Boolean!
  maxPages: Int!
  nextPage: Int
  previousPage: Int
}
```

| Field          | Description                                           |
| -------------- | ----------------------------------------------------- |
| `currentPage`  | The current page number                               |
| `hasNext`      | Whether more pages exist after the current one        |
| `hasPrevious`  | Whether pages exist before the current one            |
| `maxPages`     | Total number of pages based on totalCount and perPage |
| `nextPage`     | Page number for the next page (null if on last page)  |
| `previousPage` | Page number for the previous page (null if on first)  |

## ConnectionInterface

All paginated query responses conform to the `ConnectionInterface`:

```ts
interface ConnectionInterface<T> {
  totalCount: number
  pageInfo: PageInfoInterface
  nodes: T[]
}
```

In GraphQL, this looks like:

```graphql
type UserConnection {
  totalCount: Int!
  pageInfo: PageInfo!
  nodes: [User!]!
}
```

## Configuring maxRecordsPerPage

The maximum allowed value for `perPage` is controlled by:

1. The `API_MAX_RECORDS_PER_PAGE` environment variable (default: `2000`)
2. The `maxRecordsPerPage` option in `defineConfig`

```ts
import { defineConfig } from "@lakeql/api/config"
import { allConfigs } from "./generated/configs"

export default defineConfig({
  allConfigs,
  maxRecordsPerPage: 500, // override the env default
})
```

Requesting more than `maxRecordsPerPage` returns a validation error.

## Example query

```graphql
query PaginatedUsers {
  users(paging: { page: 2, perPage: 25 }) {
    totalCount
    pageInfo {
      currentPage
      hasNext
      hasPrevious
      maxPages
      nextPage
      previousPage
    }
    nodes {
      id
      name
      email
    }
  }
}
```

keywords: pagination paging input pageinfo output


docId: api/schema-builder/scalar-types

slug: schema-builder/scalar-types

title: Scalar Types

text: Date

text: DateTime

text: File

text: Source code

text: Usage in custom schemas

content:
---
title: Scalar Types
description: Custom GraphQL scalar types for Date, DateTime, and File uploads.
---

LakeQL registers three custom scalar types on the shared builder. These extend the standard GraphQL scalars to handle common data types from Trino.

## Date

A date-only scalar (no time component). Uses `DateResolver` from `graphql-scalars`.

- **Input:** ISO 8601 date string (e.g., `"2024-01-15"`)
- **Output:** JavaScript `Date` object

## DateTime

A full timestamp scalar with timezone support. Uses `DateTimeResolver` from `graphql-scalars`.

- **Input:** ISO 8601 datetime string (e.g., `"2024-01-15T10:30:00Z"`)
- **Output:** JavaScript `Date` object

## File

An input-only scalar for file uploads. Attempting to serialize (output) a `File` scalar throws an error.

- **Input:** `File` object (from multipart form data)
- **Output:** Not supported (throws `"Uploads can only be used as input types"`)

## Source code

```ts
import { DateResolver, DateTimeResolver } from "graphql-scalars"
import { builder } from "@lakeql/api/builder"

builder.addScalarType("Date", DateResolver, {})
builder.addScalarType("DateTime", DateTimeResolver, {})

builder.scalarType("File", {
  serialize: () => {
    throw new Error("Uploads can only be used as input types")
  },
})
```

## Usage in custom schemas

Reference these scalars by name when defining fields:

```ts
import { builder } from "@lakeql/api/builder"

const EventType = builder
  .objectRef<{
    id: string
    name: string
    createdAt: Date
    occurredOn: Date
  }>("Event")
  .implement({
    fields: (t) => ({
      id: t.exposeID("id"),
      name: t.exposeString("name"),
      createdAt: t.expose("createdAt", { type: "DateTime" }),
      occurredOn: t.expose("occurredOn", { type: "Date" }),
    }),
  })
```

keywords: scalar types custom datetime graphql


docId: api/server-setup/create-api-server

slug: server-setup/create-api-server

title: createApiServer

text: Signature

text: Return type

text: What it sets up

text: Usage

text: Starting the server

content:
---
title: createApiServer
navTitle: createApiServer
description: Create a configured Hono application with GraphQL Yoga integration, CORS, and logging middleware.
---

`createApiServer` is the primary factory function for building a LakeQL API instance. It wires together Hono, GraphQL Yoga, and all middleware into a ready-to-use server object.

## Signature

```ts
function createApiServer(options?: ApiRuntimeConfig): Promise<ApiServer>
```

## Return type

<InterfaceReference file="api/src/server" name="ApiServer" mode="declaration" />

## What it sets up

1. Creates a structured logger via `@lakeql/logger`
2. Initializes GraphQL Yoga with the loaded schema and context
3. Mounts Hono logger middleware on all routes
4. Configures CORS on the GraphQL path (POST, GET, OPTIONS)
5. Connects the Yoga request handler to Hono

## Usage

```ts path="src/server.ts"
import { createApiServer } from "@lakeql/api/server"

const { app, logger, yoga } = await createApiServer({
  baseDir: import.meta.dirname,
  schemaPath: "./schemas",
  graphqlPath: "/graphql",
  port: 4000,
})

// Add custom middleware or routes to the Hono app
app.get("/health", (c) => c.json({ status: "ok" }))
```

## Starting the server

For most projects, use `defineConfig` with `startServer()` instead of calling `createApiServer` directly:

```ts path="src/index.ts"
import { config } from "./config"

await config.startServer()
```

If you don't need `defineConfig`, `startApiServer` calls `createApiServer` internally and binds to a port:

```ts path="src/index.ts"
import { startApiServer } from "@lakeql/api/server"

await startApiServer({
  baseDir: import.meta.dirname,
  schemaPath: "./schemas",
  port: 4000,
})
```

`startApiServer` uses `@hono/node-server` to serve the application via Node.js `http` module.

navTitle: createApiServer

keywords: createapiserver create configured application graphql


docId: api/server-setup/define-config

slug: server-setup/define-config

title: defineConfig

text: Signature

text: Configuration options

text: Return type

text: Usage

content:
---
title: defineConfig
navTitle: defineConfig
description: Type-safe configuration wrapper that provides createYogaServer and startServer methods.
---

`defineConfig` is the recommended way to configure your LakeQL API server. It provides full type safety for permissions by inferring catalog, schema, and table names from your generated configs.

## Signature

```ts
function defineConfig<const TConfig extends readonly SchemaConfigEntry[]>(
  input: TConfig | DefineConfigOptions<TConfig>
): DefinedApiConfig<TConfig>
```

## Configuration options

The `DefineConfigOptions` interface extends `ApiRuntimeConfig` with a required `allConfigs` field:

<InterfaceReference file="api/src/config" name="ApiRuntimeConfig" />

## Return type

`defineConfig` returns a `DefinedApiConfig` object that includes all your config options plus two convenience methods:

```ts
interface DefinedApiConfig<TConfig> {
  // ...all config options
  allConfigs: TConfig
  createYogaServer: (logger) => Promise<ApiYoga>
  startServer: () => Promise<void>
}
```

## Usage

### Minimal — pass only configs

```ts path="src/config.ts"
import { defineConfig } from "@lakeql/api/config"
import { allConfigs } from "./config-registry"

export const config = defineConfig(allConfigs)
```

### Full configuration (matching the template)

```ts path="src/config.ts"
import { defineConfig } from "@lakeql/api/config"

import { getUser } from "./auth"
import { allConfigs } from "./config-registry"
import { permissions } from "./permissions"

const baseDir = import.meta.dirname

export const config = defineConfig({
  allConfigs,
  baseDir,
  getUser,
  graphqlPath: "/graphql",
  healthCheckEndpoint: "/live",
  permissions,
  port: 4000,
  schemaPath: "./schemas",
})
```

### Using startServer

```ts path="src/index.ts"
import { config } from "./config"

await config.startServer()
```

This calls `startApiServer` internally with your defined configuration.

navTitle: defineConfig

keywords: configuration defineconfig type-safe wrapper provides


docId: api/server-setup/yoga-configuration

slug: server-setup/yoga-configuration

title: Yoga Configuration

text: Type

text: Available overrides

text: Default behavior

text: Usage

text: Logging configuration

content:
---
title: Yoga Configuration
description: Customize GraphQL Yoga options like GraphiQL, error masking, logging, and landing page.
---

The `yogaConfig` option in `defineConfig` (or `createApiServer`) lets you override GraphQL Yoga's built-in behavior. It accepts all Yoga options except `schema` and `context`, which are managed internally.

## Type

```ts
type YogaConfigOverrides = Omit<
  NonNullable<Parameters<typeof createYoga>[0]>,
  "schema" | "context"
>
```

## Available overrides

| Option                | Type                  | Default                   | Description                              |
| --------------------- | --------------------- | ------------------------- | ---------------------------------------- |
| `healthCheckEndpoint` | `string`              | `"/live"`                 | Path for the health check endpoint       |
| `graphiql`            | `boolean \| object`   | `true` in development     | Enable/configure GraphiQL IDE            |
| `logging`             | `boolean \| LogLevel` | Based on `API_LOGGER` env | Control Yoga's internal logging          |
| `maskedErrors`        | `boolean`             | `true`                    | Hide internal error details from clients |
| `landingPage`         | `boolean`             | `false`                   | Show Yoga's default landing page         |

## Default behavior

Without any overrides, the server applies these defaults:

- **GraphiQL** is enabled when `NODE_ENV=development`
- **maskedErrors** is `true` — internal errors are replaced with generic messages
- **Logging** level is controlled by the `API_LOGGER` environment variable (default: `warn`). Set to `silent` to disable Yoga logging entirely.
- **Landing page** is disabled

## Usage

```ts path="src/config.ts"
import { defineConfig } from "@lakeql/api/config"

import { getUser } from "./auth"
import { allConfigs } from "./config-registry"
import { permissions } from "./permissions"

const baseDir = import.meta.dirname

export const config = defineConfig({
  allConfigs,
  baseDir,
  getUser,
  permissions,
  port: 4000,
  schemaPath: "./schemas",
  yogaConfig: {
    graphiql: {
      title: "LakeQL Explorer",
      defaultQuery: "{ __typename }",
    },
    maskedErrors: false, // expose full errors in development
    healthCheckEndpoint: "/healthz",
  },
})
```

## Logging configuration

The `API_LOGGER` environment variable accepts these values:

| Value    | Effect                        |
| -------- | ----------------------------- |
| `debug`  | All messages                  |
| `info`   | Info, warnings, and errors    |
| `warn`   | Warnings and errors (default) |
| `error`  | Errors only                   |
| `silent` | No Yoga logging output        |

```bash
# .env
API_LOGGER=debug
```

keywords: configuration logging customize graphql options