Marks a field of a connection’s filter input as a facet. The generated Connection type gains a facets: <ConnName>Facets field with one entry per marked filter field, each returning [<Scalar>FacetValue!] (a nullable list of non-null entries) where every entry carries a value and a count.
Each facet’s counts apply the connection’s full filter minus that facet’s own predicate. When a user has filtered rating: [PG], the rating facet still shows counts for all ratings (so the user can pivot their selection), while every other facet shows counts for the rows matching rating = PG. The paginated edges / nodes query is unaffected and applies the full filter unchanged.
value mirrors the filter field’s element type exactly, same scalar and same nullability, so a client feeds facetValue.value straight back into the filter (filter: { rating: [facetValue.value] }) with no coercion. A nullable filter element preserves the NULL bucket as its own group; a non-null element scrubs it with IS NOT NULL.
SDL signature
directive @asFacet on INPUT_FIELD_DEFINITION
Canonical example
type Query {
films(filter: FilmFilter): [Film!]! @asConnection @defaultOrder(primaryKey: true)
}
input FilmFilter {
rating: [MpaaRating!] @field(name: "RATING") @asFacet
length: [Int!] @field(name: "LENGTH") @asFacet
title: String @field(name: "TITLE")
}
Graphitron expands the Connection with:
type QueryFilmsConnectionFacets {
rating: [MpaaRatingFacetValue!]
length: [IntFacetValue!]
}
type MpaaRatingFacetValue { value: MpaaRating! count: Int! }
type IntFacetValue { value: Int! count: Int! }
A query selecting any facet field issues exactly one extra SQL statement per request: a UNION ALL of per-facet GROUP BY arms, one arm per selected facet, each under its filter-minus-self predicate. When no facet field is selected, the aggregate is skipped entirely. Each facet’s entries are ordered count descending, then within equal counts by the decoded value’s natural order (integers numerically, enums in declaration order), a preserved NULL bucket sorting after non-null values.
Failure semantics
Facets are a best-effort aggregate, never a structural guarantee. The facets object and each per-facet field are nullable, so a facet query failure or timeout degrades to null (plus an entry in the GraphQL errors array) without propagating through non-null bubbling to the connection or the request; the page of results still returns.
Constraints
-
Valid only on fields of an input type used as the filter input of at least one
@asConnection-bearing field; an input type no connection consumes is rejected at build time (the expansion would be dead schema). An input shared by connection and non-connection consumers is fine: the directive surfaces facets at the connection use sites and is inert at the others. -
The field must be a plain
@field(name:)-bound scalar or enum column.@reference/@condition/@nodeIdbindings,ID-typed fields, and input-object fields are rejected (join-mediated and node-ID facets are a follow-up). -
The field must be nullable (optional): an always-active filter value could never show unfiltered pivot counts, and the generated filter-minus-self fragments suppress a facet’s own predicate by leaving it unset.
-
v1 serves facets only on root
Queryconnections over a@table-backed object element. A faceted child (@splitQuery) or interface/union connection is rejected at build time (its facets would resolve to null); lifting this is a follow-up. -
A facet’s name must be unique across the carrier’s whole filter surface (every filter input’s fields and every top-level argument): the facet fragments and the generated condition method key parameters by name, so any same-named sibling filter is rejected at build time.
-
Composes with the deprecated
@asConnection(connectionName:)override: the generator resolves a faceted carrier by its field coordinate, so an overridden connection name synthesises its facet surface (<ConnName>Facetsand the facet value types) under the overridden name. The override itself remains deprecated; see@asConnection. -
Facet synthesis applies to directive-driven
@asConnectioncarriers only; a hand-written structural Connection type is author-owned and never gains afacetsfield. Sharing a filter input between a served root carrier and a structural carrier is legal (the directive is inert at the structural consumer), but an input consumed only by structural connections is rejected: its facets could never surface anywhere.
See also
-
@asConnectionowns the Connection synthesis the facets ride on. -
@fieldbinds the facet to its column. -
@defaultOrderanchors the connection’s pagination; facets do not affect it.