Marks a list field as a Relay cursor connection. The schema-transform stage rewrites the field’s return type from [T!]! to a synthesized TConnection carrying edges, nodes, pageInfo, and totalCount; the generator emits keyset-based seek/limit SQL with ROW_NUMBER() OVER (PARTITION BY …) for batched per-parent pagination. If the field doesn’t already declare pagination arguments, forward-pagination args (first: Int, after: String) are added automatically; backward (last/before) and dynamic @orderBy arguments coexist when present.
|
Names from inside the generator. This page names types from graphitron’s own classification model. They are accurate today, and worth knowing when a rejection message quotes one back at you, but they are not part of the authoring contract: the classification walk that produces them is being drained, and these names retire with it. What stays stable is the schema you write, the directive reference that describes it, and the diagnostics glossary. |
@asConnection works on three field shapes that the rewrite recognises distinctly: bare list fields on @table parents (inline pagination), @reference-driven child lists (per-parent paginated children), and union/interface lists (multi-table polymorphic pagination). It also composes with @splitQuery to dispatch each parent’s connection through a DataLoader.
SDL signature
directive @asConnection(
defaultFirstValue: Int = 100
connectionName: String
) on FIELD_DEFINITION
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
|
|
|
Page size used when the client omits |
|
|
|
Deprecated. Override the synthesized connection type’s name. Each connection field should own its own type; sharing a Connection across fields conflates distinct semantic contexts (different parents, filters, orders) at the type level. The override remains supported as a transition mechanism for legacy schemas but is permanently discouraged. See How-to: Cursor-paginated connections. |
Canonical examples
A bare list on a @table parent (Query.stores): @asConnection rewrites the return type to StoreConnection, adds first:/after: arguments, and emits keyset pagination using @defaultOrder for the cursor:
type Query {
stores: [Store!]! @asConnection @defaultOrder(primaryKey: true)
}
A union-typed list (Address.occupantsConnection): the multi-table polymorphic emitter wraps the per-branch UNION ALL in a derived table so seek/limit and cursor pagination apply across the union; totalCount counts only this parent’s occupants:
type Address @table(name: "address") {
occupants: [AddressOccupant!]!
occupantsConnection: [AddressOccupant!]! @asConnection(defaultFirstValue: 5)
}
union AddressOccupant = Customer | Staff
Per-parent paginated children with @splitQuery (Film.actorsConnection): a ROW_NUMBER() OVER (PARTITION BY parentInput.idx) envelope gives each parent its own paginated slice, and a DataLoader batches the dispatch:
type Film @table(name: "film") {
actorsConnection(
first: Int, last: Int, after: String, before: String
): ActorsConnection! @splitQuery @reference(path: [
{key: "film_actor_film_id_fkey"},
{key: "film_actor_actor_id_fkey"}
]) @defaultOrder(primaryKey: true)
}
When the schema declares the connection type explicitly (the ActorsConnection! shape above), the rewrite reuses it instead of synthesising a fresh one. Otherwise, a *Connection, *Edge, and PageInfo are generated alongside the field.
Federation directives on the field (@shareable, @tag) propagate onto the synthesised Connection, Edge, and PageInfo types, so federation contracts that include the field’s tags also include its pagination types. An author-declared PageInfo is left untouched; a synthesised one carries the union of the tags from every connection-promoted field.
Dynamic order via @orderBy composes naturally:
actorsOrderedConnection(
order: [ActorOrderBy] @orderBy,
first: Int, last: Int, after: String, before: String
): ActorsConnection! @splitQuery @reference(path: [
{key: "film_actor_film_id_fkey"},
{key: "film_actor_actor_id_fkey"}
]) @defaultOrder(primaryKey: true)
The cursor encodes the active order columns (the @orderBy selection if the client supplied one, otherwise @defaultOrder); decoding rejects cursors whose embedded sort doesn’t match the request’s effective order.
Constraints
-
Applies to list-typed fields. The runtime cardinality must be a list; single-cardinality
@asConnectionis rejected at classify time. -
The field must have a stable order. Either
@defaultOrderis set on the field, or@orderByis required at runtime; without one of these, keyset pagination has no anchor and the build fails. -
The synthesized connection type’s name must not collide with another type already declared in the schema. The default
<ParentType><FieldName>Connectionshape rarely collides because parent and field names jointly disambiguate; collisions usually mean two carriers should be split into distinct fields rather than coalesced. The deprecatedconnectionName:override exists as a transition escape hatch. -
defaultFirstValue:is the page size used when the client omitsfirst:; it is not a cap. The rewrite plugin does not ship a server-side maximum onfirst:/last:(the legacy generator’s<maxAllowedPageSize>setting was excised); enforce caps at the API layer if needed. See How-to: Cursor-paginated connections. -
Composes with
@splitQuery: when both are present, the connection is materialized per-parent through aDataLoaderkeyed on the parent’s PK or FK. Without@splitQueryon a parent-keyed list, all parents' rows are paginated in a single SQL pass (thePARTITION BYenvelope is omitted). -
Inline
TableFieldplacement is not allowed under interface-or-union return types unless the surrounding type axis supports the multi-table emitter; the generator rejects mismatched placements at classify time. (See Graphitron Development Principles for the "no generator-side invariant goes unchecked at validate time" rule that backs this.)
See also
-
@splitQuerydispatches per-parent connections through aDataLoaderfor nested-depth pagination. -
@defaultOrderanchors the cursor when the client omits@orderBy. -
@orderBysupplies dynamic order; the cursor encodes the active selection. -
How-to: Cursor-paginated connections covers the one-field-one-connection rule,
totalCountperformance, and what the rewrite does about page-size caps.