Generated code is intentionally simple: no auth checks, no transaction management, no tenant-aware connection routing. These concerns belong at runtime, injected through GraphitronContext without touching generated code.

This document is the reference for the rewrite-emitted GraphitronContext interface. For the minimal wiring that stands up a working endpoint, see Getting Started; this file picks up where that leaves off.


GraphitronContext

GraphitronContext is the sealed per-request contract every generated DataFetcher reads from. It is emitted per app under <outputPackage>.schema and sealed to permit only the generator’s own GraphitronContextImpl singleton (nested inside the interface to inherit same-compilation-unit permits); apps no longer implement it directly. The extension surface moves to the points where per-request values cross the boundary:

  • Per-request DSLContext: pass it as the first parameter of Graphitron.newExecutionInput(dsl, …​). Per-tenant routing is the subject of R45’s tenant-column work; until that lands, single-tenant apps pass a single DSLContext per request.

  • Per-request contextArgument values: pass each one as a typed parameter to Graphitron.newExecutionInput(…​). The factory’s parameter list reflects the schema’s declared contextArguments and their reflected Java types, so the consumer’s request-entry code threads each value through a typed slot rather than stashing arbitrary entries on GraphQLContext by hand.

  • Custom validator factory: covered by a follow-up Mojo configuration item (R192). The default reads Validation.buildDefaultValidatorFactory().getValidator(); this is unchanged by R190 and the single existing override point is currently unused, so no migration is required.

// GENERATED (illustrative shape; full method set evolves with the schema)
public sealed interface GraphitronContext {
    default DSLContext getDslContext(DataFetchingEnvironment env) { ... }
    default Object getContextArgument(DataFetchingEnvironment env, String name) { ... }
    default Validator getValidator(DataFetchingEnvironment env) { ... }

    final class GraphitronContextImpl implements GraphitronContext {
        public static final GraphitronContextImpl INSTANCE = new GraphitronContextImpl();
        private GraphitronContextImpl() {}
    }
}

getContextArgument reads the value from env.getGraphQlContext() and returns it as Object; the cast to the expected Java type happens at the generated call site ((String) graphitronContext(env).getContextArgument(env, "userId")). A missing entry throws IllegalStateException naming the contextArgument and pointing at Graphitron.newExecutionInput(…​); a wrong-typed entry surfaces as ClassCastException at the generated cast. Both paths are only reachable when a consumer hand-rolls an ExecutionInput.Builder outside Graphitron.newExecutionInput(…​); the typed factory makes the same mistake a compile error at the call site. The framework’s redact path replaces the prose message with a correlation-id reference before it reaches the consumer, so the runtime throw is server-log surface only; the typed factory’s parameter list is the load-bearing diagnostic.

Where the interface comes from

The interface is emitted per app, not imported from a shared module. Every code-generation run produces one GraphitronContext.java file under <outputPackage>.schema, written by GraphitronContextInterfaceGenerator. The generated interface depends only on graphql-java’s DataFetchingEnvironment and jOOQ’s DSLContext; it does not pull in any Graphitron runtime jar. The sealed declaration plus nested impl mean the contract is global to the build (one impl per app), with all per-request state living in the GraphQLContext the factory populates.

Where each per-request value comes from

Generated fetchers retrieve the singleton through a private helper emitted once per *Fetchers class:

// GENERATED: from TypeFetcherGenerator.buildGraphitronContextHelper
private static GraphitronContext graphitronContext(DataFetchingEnvironment env) {
    return env.getGraphQlContext().get(GraphitronContext.class);
}

The singleton’s default methods then read the per-request values back from env.getGraphQlContext(): DSLContext.class for the per-request DSLContext, the contextArgument’s string name for each typed value the factory put. The full minimum viable wiring (building the schema, wiring GraphQL, calling the factory) lives in Getting Started > Hello world.

sequenceDiagram
    participant Client
    participant Engine as graphql-java engine
    participant Fetcher as generated DataFetcher
    participant Ctx as GraphitronContext<br/>(consumer impl)
    participant DSL as DSLContext
    participant DB as PostgreSQL

    Client->>Engine: ExecutionInput<br/>(query, populated GraphQLContext, DataLoaderRegistry)
    Engine->>Fetcher: invoke (DataFetchingEnvironment env)
    Fetcher->>Ctx: graphitronContext(env).getDslContext(env)
    Ctx-->>Fetcher: per-request DSLContext<br/>(read from env.getGraphQlContext().get(DSLContext.class))
    Fetcher->>DSL: select(...).from(...).where(...)
    DSL->>DB: SQL (driven by selection set)
    DB-->>DSL: Result&lt;Record&gt;
    DSL-->>Fetcher: rows
    Fetcher->>Ctx: getContextArgument(env, "userId")<br/>(if @condition uses contextArguments)
    Ctx-->>Fetcher: claim value (Java cast applied at call site)
    Fetcher-->>Engine: Result&lt;Record&gt; / ConnectionResult
    Engine-->>Client: response (graphql-java traverses records via field DataFetchers)

The diagram shows one fetcher invocation; per-request behaviour (tenant routing in getDslContext, session variables for RLS, JWT claims through getContextArgument) all happen on the request thread, scoped to the DataFetchingEnvironment. The generator emits the call sites; the consumer’s GraphitronContext implementation decides what each call returns.

getDslContext: database access

Every generated query method calls getDslContext(env) to obtain the DSLContext for executing SQL. The default impl reads env.getGraphQlContext().get(DSLContext.class), populated by the factory’s defaultDsl parameter. This is the only impl; the sealed interface closes off ad-hoc overrides. To return a tenant-specific DSLContext per request, build the right DSLContext at request entry and pass it to Graphitron.newExecutionInput(dsl, …​) (reintroduced under R45’s tenant-column work). The same shape composes with PostgreSQL row-level security; see Row-Level Security below.

getContextArgument: passing runtime values into generated methods

getContextArgument passes values from the GraphQL context into generated condition and method calls. It is invoked when a method parameter is classified as ParamSource.Context, driven by the contextArguments field on the @condition, @service, and @tableMethod directives.

For example, a field with @condition(condition: "AccessControl.visibleToUser", contextArguments: ["userId"]) produces:

// GENERATED
condition = condition.and(AccessControl.visibleToUser(table,
    (String) graphitronContext(env).getContextArgument(env, "userId")));

The Java cast at the generated call site is reflected from the developer method’s parameter type, so it matches the compile-time signature the consumer wrote against. The supplying side is the factory’s typed parameter slot: Graphitron.newExecutionInput(dsl, userId) with userId: String reflects straight into the consumer’s method, and a typo in the SDL contextArguments: list surfaces as a compile error at the factory call (not a null at request time).


Complementary Technologies

The sections below describe standard capabilities that compose naturally with GraphitronContext. They are not Graphitron-specific extension points: they work because getDslContext gives you full control over the DSLContext and its configuration.

Where each concern belongs. Three layers, in order of preference:

  • jOOQ Configuration for cross-cutting jOOQ behaviour: type converters, forced types, RecordMapperProvider, naming strategies. Configured once per app and shared by every DSLContext you return from getDslContext.

  • The DSLContext you pass into Graphitron.newExecutionInput for per-request decisions: which tenant’s DSLContext to use, which session variables to SET LOCAL, which connection to acquire. Anything that varies request-by-request gets composed at request entry and threaded through the factory.

  • Schema directives (@condition, @tableMethod, @reference) for predicates and joins that are part of the schema’s business semantics. Anything a schema author should be able to read in the SDL belongs here, not in a runtime hook.

ExecuteListener is an advanced jOOQ-level hook (logging, metrics, query rewriting); use it sparingly and prefer Configuration or a directive when either fits.

Instance @service holders

Instance @service classes are constructed per call via new ClassName(DSLContext). The holder is created fresh per fetcher invocation; do not stash request-scoped state on instance fields, since each call gets its own holder. For per-tenant or per-request decisions inside the holder, ride into the holder via the DSLContext’s `Configuration: Configuration.data(key) carries ad-hoc per-request values written by your getDslContext implementation, and Configuration.settings() plus registered Converter / RecordMapperProvider carry cross-cutting jOOQ behaviour. For SDL-visible per-request arguments, prefer a static method that takes the value via getContextArgument. The single-(DSLContext) constructor contract is a deliberate constraint, mirroring the legacy generator: every per-request seam already exists on Configuration or in the SDL, so a wider holder ctor would duplicate plumbing without adding capability.

jOOQ Configuration

For most applications, jOOQ’s Configuration is the most important extension point. DefaultConfiguration controls type converters, forced types, synthetic primary keys, embedded records, naming strategies, and more. These settings flow through every query Graphitron generates.

var config = new DefaultConfiguration();
config.set(SQLDialect.POSTGRES);
config.set(dataSource);
// Type converters, forced types, RecordMapperProvider, etc. go here
config.set(new DefaultRecordMapperProvider());
DSLContext ctx = DSL.using(config);

See the jOOQ Configuration documentation for the full set of options.

jOOQ ExecuteListener

ExecuteListener is an advanced hook that intercepts query execution at lifecycle points (before rendering, before execution, after execution, etc.). Most applications do not need this: it is mainly useful for SQL logging, metrics collection, or query rewriting. Register a listener on DefaultConfiguration before creating the DSLContext; see the jOOQ ExecuteListener documentation for the full lifecycle and available hooks.

Row-Level Security

Graphitron’s security model designates the database as the enforcement point. Row-level security (RLS) is the recommended mechanism: policies filter rows transparently based on session context, with no changes to generated queries.

Most relational databases support a form of row-level security. The example below uses PostgreSQL.

The connection point is getDslContext: set the session context before returning the DSLContext, and RLS policies read those values for every query.

-- ILLUSTRATIVE: PostgreSQL RLS policy
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON documents
    USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
// ILLUSTRATIVE: setting session context in getDslContext
@Override
public DSLContext getDslContext(DataFetchingEnvironment env) {
    String tenantId = env.getGraphQlContext().get("tenantId");

    // Hold a single connection for the request so the SET and all queries share it.
    // Use set_config with isLocal=true (transaction-scoped) for connection pool safety:
    // this resets automatically when the transaction ends rather than persisting on the
    // connection after it returns to the pool.
    Connection conn = dataSource.getConnection();
    DSLContext ctx = DSL.using(conn, SQLDialect.POSTGRES);
    ctx.execute("SELECT set_config('app.tenant_id', ?, true)", tenantId);
    return ctx;
}

Generated code is unaware of RLS: it issues plain SELECT statements, and the database enforces the policies automatically.


See also:

  • Getting Started: Hello world, custom scalar, federation, tenant-scoped DSLContext, JWT-claim context arguments

  • Code Generation Triggers: Schema patterns to sealed variants, including the @condition / @tableMethod directives that drive getContextArgument calls

  • Security Model: Database-level security philosophy