Skip to content

Commit 119f2c5

Browse files
committed
refactor AbstractFilterArgument back to one document
1 parent da6bd2d commit 119f2c5

File tree

3 files changed

+174
-139
lines changed

3 files changed

+174
-139
lines changed
Lines changed: 149 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,4 @@
1-
# GraphQL Abstract Type Filter Specification
2-
3-
_Status: Strawman_<br>
4-
_Version: 2026-01-08_
5-
6-
This specification aims to provide a standardized way for clients to communicate
7-
the exclusive set of types permitted in a resolver’s response when returning one
8-
or more abstract types (i.e. an Interface or Union return type).
9-
10-
Algorithms are provided for resolvers to enforce this contract at runtime.
11-
12-
In the following example, `allPets` will return **only** `Cat` or `Dog` types:
13-
14-
```graphql example
15-
{
16-
allPets(only: ["Cat", "Dog"]) {
17-
... on Cat { name }
18-
... on Dog { name }
19-
}
20-
}
21-
```
22-
23-
This is enforced on the server when using the `@limitTypes` type system
24-
directive.
25-
26-
This specification is intended to be used in conjunction with the
27-
[GraphQL @matches Directive Specification](./MatchesSpec.html) in order to avoid
28-
duplicating the list of allowed types passed as a field argument.
29-
30-
**Use Cases**
31-
32-
Applications may implement this specification to provide a filter for what
33-
type(s) may be returned by a resolver. Notably, the filtering happens on the
34-
server side, allowing clients to guarantee a fixed length of results.
35-
36-
This may also be used a versioning scheme by applications that dynamically
37-
render different parts of a user interface mapped from the return type(s) of a
38-
resolver. Each version of the application can define the exclusive set of types
39-
it supports displaying in the user interface.
1+
# Abstract Type Filter Argument
402

413
## @limitTypes
424

@@ -45,8 +7,11 @@ directive @limitTypes on ARGUMENT_DEFINITION
457
```
468

479
`@limitTypes` is a type system directive that may be applied to a field
48-
argument in order to express that it defines the exclusive set of types that the
49-
field may return.
10+
argument definition in order to express that it will define the exclusive set of
11+
types that the field is allowed to return.
12+
13+
The server must enforce and validate the allowed types according to this
14+
specification.
5015

5116
**Example Usage**
5217

@@ -66,10 +31,6 @@ type Cat implements Pet {
6631
type Dog implements Pet {
6732
name: String!
6833
}
69-
70-
interface Human {
71-
name: String!
72-
}
7334
```
7435

7536
`@limitTypes` may also be applied to schema that implements the
@@ -93,10 +54,6 @@ type PetEdge {
9354
cursor: String!
9455
node: Pet
9556
}
96-
97-
interface Pet {
98-
name: String!
99-
}
10057
```
10158

10259
## Schema Validation
@@ -112,46 +69,27 @@ returns either:
11269

11370
- an abstract type
11471
- a list of an abstract type
115-
- a connection type (conforming to
116-
[the GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm#sec-Connection-Types)) over an abstract type
72+
- a connection type (conforming to the
73+
[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm#sec-Connection-Types)
74+
over an abstract type)
11775

11876
## Execution
11977

12078
The `@limitTypes` directive places requirements on the {resolver} used to
121-
satisfy the field. Implementers of this specification must honour these
79+
satisfy the field. Implementers of this specification must honor these
12280
requirements.
12381

124-
### Filter Argument Validation
125-
126-
:: A *filter argument* is the coerced argument value of a field argument with
127-
the `@limitTypes` directive applied.
128-
129-
Each type referenced in the *filter argument* must exist in the type system,
130-
and be a possible return type of the field to be considered a valid argument in
131-
the context of this specification.
132-
133-
This validation happens as part of {CoerceAllowedTypes()}, defined below.
134-
135-
```graphql counter-example
136-
{
137-
allPets(only: ["Cat", "Dog", "LochNessMonster"]) {
138-
name
139-
}
140-
}
141-
```
142-
143-
The example above must yield an execution error, since `LochNessMonster` is not
144-
a type that exists in the type system.
82+
### Coercing Allowed Types
14583

146-
Note: Filter argument validation is necessary as schema-unaware clients are
147-
otherwise unable to verify the correctness of this argument.
84+
:: A *filter argument* is a field argument which has the `@limitTypes`
85+
directive applied.
14886

149-
### Coerce Allowed Types
87+
The input to the *filter argument* is a list of strings, however this must be
88+
made meaningful to the resolver such that it may perform its filtering - thus we
89+
must resolve it into a list of valid concrete object types that are possible in
90+
this position.
15091

151-
The input to the filter argument is a list of strings, however this must be made
152-
meaningful to the resolver such that it may perform its filtering - thus we must
153-
resolve it into a list of valid concrete object types that are possible in this
154-
position.
92+
:: The coerced list of valid concrete object types are the *allowed types*.
15593

15694
CoerceAllowedTypes(abstractType, typeNames):
15795

@@ -162,7 +100,7 @@ CoerceAllowedTypes(abstractType, typeNames):
162100
- If {type} does not exist, raise an execution error.
163101
- If {type} is an object type:
164102
- If {type} is a member of {possibleTypes}, add {type} to {allowedTypes}.
165-
- Otherwise, raise an exection error.
103+
- Otherwise, raise an execution error.
166104
- Otherwise, if {type} is a union type:
167105
- For each {concreteType} in {type}:
168106
- If {concreteType} is a member of {possibleTypes}, add {concreteType} to
@@ -171,23 +109,76 @@ CoerceAllowedTypes(abstractType, typeNames):
171109
- For each {concreteType} that implements {type}:
172110
- If {concreteType} is a member of {possibleTypes}, add {concreteType} to
173111
{allowedTypes}.
174-
- Otherwise continue to the next {typeName}.
112+
- Otherwise, raise an execution error (scalars, enums, and input types are not
113+
valid filter argument values).
175114
- Return {allowedTypes}.
176115

177-
### Enforcing Allowed Types
116+
**Explanatory Text**
117+
118+
The input to the *filter argument* may include both concrete and abstract types.
119+
{CoerceAllowedTypes} expands *allowed types* to include the possible and valid
120+
concrete types for each abstract type.
121+
122+
To see why this is needed, we will expand our example schema above to include
123+
the following types:
124+
125+
```graphql example
126+
interface Fish {
127+
swimSpeed: Int!
128+
}
129+
130+
type Goldfish implements Pet & Fish {
131+
name: String!
132+
swimSpeed: Int!
133+
}
134+
135+
type Haddock implements Fish {
136+
swimSpeed: Int!
137+
}
138+
```
178139

179-
Enforcement of the allowed types is the responsibility of the {resolver} called
180-
in
140+
It is possible for types to implement multiple interfaces. It therefore must be
141+
possible to select concrete types of another interface in the *filter argument*:
142+
143+
```graphql example
144+
{
145+
allPets(only: ["Fish"]) {
146+
... on Goldfish {
147+
swimSpeed
148+
}
149+
}
150+
}
151+
```
152+
153+
The below example must fail, since `Haddock` does not implement the `Pet`
154+
interface, and is therefore not a possible return type.
155+
156+
```graphql counter-example
157+
{
158+
allPets(only: ["Haddock"]) {
159+
... on Fish {
160+
swimSpeed
161+
}
162+
}
163+
}
164+
```
165+
166+
### Allowed Types Restriction
167+
168+
Enforcement of the *allowed types* is the responsibility of the {resolver}
169+
called in
181170
[`ResolveFieldValue()`](<https://spec.graphql.org/draft/#ResolveFieldValue()>)
182171
during the [`ExecuteField()`](<https://spec.graphql.org/draft/#ExecuteField()>)
183-
algorithm. This is because the filtering must be applied to the {collection}
184-
prior to applying the pagination arguments.
172+
algorithm.
185173

186-
When the field returns an abstract type, the {collection} is a list containing
187-
a single element which is that type. When the field returns a list of an
188-
abstract type, the {collection} is this list. When the field returns a
189-
connection type over an abstract type, the {collection} is the list of abstract
190-
type the connection represents.
174+
:: When the field returns an abstract type, the *collection* is this type.
175+
When the field returns a list of an abstract type, the *collection* is this
176+
list. When the field returns a connection type over an abstract type, the
177+
*collection* is the list of abstract type the connection represents.
178+
179+
The resolver must apply this restriction when fetching or generating the source
180+
data to produce the *collection*. This is because the filtering must occur prior
181+
to applying pagination logic in order to produce the correct number of results.
191182

192183
When a field with a `@limitTypes` argument is being resolved:
193184

@@ -201,19 +192,71 @@ When a field with a `@limitTypes` argument is being resolved:
201192
- Let {abstractType} be the abstract type the {collection} represents.
202193
- Let {allowedTypes} be {CoerceAllowedTypes(abstractType, limitTypes)}.
203194

204-
The resolver must ensure that the result of
205-
[`ResolveAbstractType()`](<https://spec.graphql.org/draft/#ExecuteField()>) for
206-
each entry in {collection} is a type within {allowedTypes}. The resolver must
207-
apply this restriction before applying any pagination arguments.
208-
209195
Note: The restriction must be applied before pagination arguments so that
210-
non-terminal pages in the {collection} get full representation - i.e. there are
211-
no gaps.
196+
non-terminal pages in the collection get full representation - i.e. there
197+
are no gaps.
198+
199+
## Validation Algorithms
212200

213-
### Field Collection Validation
201+
`@limitTypes` fields must implement the algorithms listed in the
202+
[Execution](#Execution) section above to be spec-compliant. However, it may be
203+
impossible or extremely difficult for GraphQL servers to statically verify the
204+
correctness of the runtime and prevent non-compliant implementations.
214205

215-
TODO: the following should raise an error since `Mouse` does not appear as a
216-
value in {allowedTypes}
206+
To this end, this section specifies a set of algorithms in order for the server
207+
to validate that the *filter argument* value and the field response are valid.
208+
209+
Usage of these algorithms is **optional**, but highly recommended to guard
210+
against programmer error.
211+
212+
All algorithms in this section run either before or after
213+
[`ResolveFieldValue()`](<https://spec.graphql.org/draft/#ResolveFieldValue()>),
214+
and must be run automatically by the server when executing fields for which
215+
the `@limitTypes` directive is applied,
216+
217+
### Filter Argument Value Validation
218+
219+
Each member of the *filter argument* value must exist in the type system and be
220+
a possible return type of the field.
221+
222+
For example, the query below must yield an execution error - since
223+
`LochNessMonster` is not a type that exists in the example schema.
224+
225+
```graphql counter-example
226+
{
227+
allPets(only: ["Cat", "Dog", "LochNessMonster"]) {
228+
name
229+
}
230+
}
231+
```
232+
233+
When used, this algorithm must be applied before
234+
[`ResolveFieldValue()`](<https://spec.graphql.org/draft/#ResolveFieldValue()>).
235+
236+
ValidateFilterArgument(filterArgumentValue):
237+
238+
- Let {abstractType} be the abstract type the {collection} represents.
239+
- Let {possibleTypes} be a set of the possible types of {abstractType}.
240+
- For each {typeName} in {filterArgumentValue}:
241+
- Let {type} be the type in the schema named {typeName}.
242+
- If {type} does not exist, raise an execution error.
243+
- If {type} is an object type:
244+
- If {type} is not a member of {possibleTypes} raise an execution error.
245+
- Otherwise, if {type} is a union type:
246+
- ??? todo
247+
- Otherwise, if {type} is an interface type:
248+
- ??? todo
249+
- Otherwise, raise an execution error (scalars, enums, and input types are not
250+
valid filter argument values).
251+
- Return {allowedTypes}.
252+
253+
Note: Schema-aware clients or linting tools are encouraged to implement this
254+
validation locally.
255+
256+
### Field Collection Validation (wip)
257+
258+
For example, the following query must raise an execution error since `Mouse`
259+
does not appear as a value in {allowedTypes}
217260

218261
```graphql counter-example
219262
{
@@ -225,6 +268,8 @@ value in {allowedTypes}
225268
}
226269
```
227270

271+
TODO: implement algorithm
272+
228273
### Field Response Validation (wip)
229274

230275
TODO: if the response array of the field contains a type that did not appear in
@@ -256,3 +301,5 @@ likely is not possible since this logic is intended to be run generically as a
256301
middleware - i.e _after_ the field has completed, and the in-memory object
257302
representation has been converted into json blob (potentially without
258303
`__typename`)
304+
305+
or can we look at using \_\_resolveType()?
Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,4 @@
1-
# GraphQL @matches Directive Specification
2-
3-
_Status: Strawman_<br>
4-
_Version: 2026-01-08_
5-
6-
`@matches` is an executable directive that clients or code generation tools
7-
may provide in order to generate the input value for a field argument which uses
8-
`@limitTypes` type system directive.
9-
10-
Users of this specification must also implement
11-
the [GraphQL Abstract Type Filter Specification](./AbstractFilterSpec.html)
12-
on the server in order to enforce the type matching contract at runtime.
13-
14-
Note: Usage of `@matches` is optional, but recommended to avoid duplication of
15-
the list of allowed types.
1+
# @matches Directive
162

173
## @matches
184

@@ -22,6 +8,13 @@ directive @matches(
228
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
239
```
2410

11+
*@matches* is an executable directive that clients or code generation tools
12+
may provide in order to generate the input value for a field argument which uses
13+
`@limitTypes` type system directive.
14+
15+
Note: Usage of `@matches` is optional, but recommended to avoid duplication of
16+
the list of allowed types.
17+
2518
**Directive Arguments**
2619

2720
argument:
@@ -117,12 +110,12 @@ CollectAllowedTypes(selectionSet):
117110
- For each {edgeSelection} in {edgesSelectionSet}:
118111
- If {edgeSelection} is a Field and its name is {"node"}:
119112
- Let {nodeSelectionSet} be the selection set of {edgeSelection}.
120-
- Let {nodeTypes} be CollectAllowedTypes({nodeSelectionSet}).
113+
- Let {nodeTypes} be {CollectAllowedTypes(nodeSelectionSet)}.
121114
- Add each type in {nodeTypes} to {allowedTypes}.
122115
- Return {allowedTypes}.
123116

124117
<!--
125-
TODO: handle the following case
118+
TODO: ~handle~ explicitly disallow the following case
126119
127120
allPetsConnection @matches {
128121
... on ConnectionType { pageInfo { } } # Fragment on the Connection type itself
@@ -133,8 +126,6 @@ TODO: handle the following case
133126
}
134127
}
135128
}
136-
137-
CollectAllowedTypes needs to branch based on if it's a connection or not.
138129
-->
139130

140131
TransformDocument(document):
@@ -145,7 +136,7 @@ TransformDocument(document):
145136
- Let {argumentName} be the argument value of the {"argument"} argument of {matchesDirective}
146137
- If {field} has an argument named {argumentName}, raise an error.
147138
- Let {selectionSet} be the selection set of {field}.
148-
- Let {allowedTypes} be CollectAllowedTypes({selectionSet}).
139+
- Let {allowedTypes} be {CollectAllowedTypes(selectionSet)}.
149140
- Let {typeNames} be a list of the names of each type in {allowedTypes}.
150141
- Add an argument named {argumentName} with value {typeNames} to {field}.
151142
- Remove {matchesDirective} from {field}.

0 commit comments

Comments
 (0)