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 ofGraphitron.newExecutionInput(dsl, …). Per-tenant routing is the subject of R45’s tenant-column work; until that lands, single-tenant apps pass a singleDSLContextper request. -
Per-request
contextArgumentvalues: pass each one as a typed parameter toGraphitron.newExecutionInput(…). The factory’s parameter list reflects the schema’s declaredcontextArgumentsand their reflected Java types, so the consumer’s request-entry code threads each value through a typed slot rather than stashing arbitrary entries onGraphQLContextby 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<Record>
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<Record> / 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
Configurationfor cross-cutting jOOQ behaviour: type converters, forced types,RecordMapperProvider, naming strategies. Configured once per app and shared by everyDSLContextyou return fromgetDslContext. -
The
DSLContextyou pass intoGraphitron.newExecutionInputfor per-request decisions: which tenant’sDSLContextto use, which session variables toSET 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/@tableMethoddirectives that drivegetContextArgumentcalls -
Security Model: Database-level security philosophy