Skip to content

Commit a7ca6b3

Browse files
authored
Update Abstract Filter Argument spec to include @matches spec (#1881)
* Update AbstractFilter spec to include @matches spec * split specs
1 parent 999087d commit a7ca6b3

File tree

3 files changed

+378
-21
lines changed

3 files changed

+378
-21
lines changed

rfcs/AbstractFilter/AbstractFilterArgumentSpec.md

Lines changed: 159 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,81 @@
1-
# Abstract Filter Argument
1+
# GraphQL Abstract Type Filter Specification
22

3-
Status: Strawman
3+
_Status: Strawman_<br>
4+
_Version: 2026-01-08_
45

5-
## Directive
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.
40+
41+
## @limitTypes
642

743
```graphql
844
directive @limitTypes on ARGUMENT_DEFINITION
945
```
1046

11-
### Examples
47+
`@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.
1250

13-
```graphql
51+
**Example Usage**
52+
53+
```graphql example
1454
type Query {
15-
allPets(first: Int, only: [String] @limitTypes): [Pet]
55+
allPets(only: [String] @limitTypes): [Pet]
1656
}
1757

1858
interface Pet {
1959
name: String!
2060
}
61+
62+
type Cat implements Pet {
63+
name: String!
64+
}
65+
66+
type Dog implements Pet {
67+
name: String!
68+
}
69+
70+
interface Human {
71+
name: String!
72+
}
2173
```
2274

23-
```graphql
75+
`@limitTypes` may also be applied to schema that implements the
76+
[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm#sec-Connection-Types):
77+
78+
```graphql example
2479
type Query {
2580
allPetsConnection(
2681
first: Int
@@ -29,7 +84,15 @@ type Query {
2984
): PetConnection
3085
}
3186

32-
# TODO: connection types
87+
type PetConnection {
88+
edges: [PetEdge]
89+
pageInfo: PageInfo!
90+
}
91+
92+
type PetEdge {
93+
cursor: String!
94+
node: Pet
95+
}
3396

3497
interface Pet {
3598
name: String!
@@ -44,18 +107,45 @@ same field.
44107
The `@limitTypes` directive may only appear on an argument that accepts a
45108
(possibly non-nullable) list of (possibly non-nullable) String.
46109

47-
The `@limitTypes` directive may only appear on an argument to a field whose
48-
named return type is a connection type (that is to say, conforming to
49-
[the GraphQL Cursor Connections Specification's Connection Type](https://relay.dev/graphql/connections.htm#sec-Connection-Types))
50-
over an abstract type, or to a field whose return type is a list and the named
51-
return type is an abstract type.
110+
The `@limitTypes` directive may only appear on an field argument where the field
111+
returns either:
112+
113+
- an abstract type
114+
- 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
52117

53118
## Execution
54119

55120
The `@limitTypes` directive places requirements on the {resolver} used to
56121
satisfy the field. Implementers of this specification must honour these
57122
requirements.
58123

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.
145+
146+
Note: Filter argument validation is necessary as schema-unaware clients are
147+
otherwise unable to verify the correctness of this argument.
148+
59149
### Coerce Allowed Types
60150

61151
The input to the filter argument is a list of strings, however this must be made
@@ -69,9 +159,10 @@ CoerceAllowedTypes(abstractType, typeNames):
69159
- Let {allowedTypes} be an empty unordered set of object types.
70160
- For each {typeName} in {typeNames}:
71161
- Let {type} be the type in the schema named {typeName}.
72-
- If {type} does not exist, continue to the next {typeName}.
162+
- If {type} does not exist, raise an execution error.
73163
- If {type} is an object type:
74164
- If {type} is a member of {possibleTypes}, add {type} to {allowedTypes}.
165+
- Otherwise, raise an exection error.
75166
- Otherwise, if {type} is a union type:
76167
- For each {concreteType} in {type}:
77168
- If {concreteType} is a member of {possibleTypes}, add {concreteType} to
@@ -92,9 +183,11 @@ during the [`ExecuteField()`](<https://spec.graphql.org/draft/#ExecuteField()>)
92183
algorithm. This is because the filtering must be applied to the {collection}
93184
prior to applying the pagination arguments.
94185

95-
When the field returns a list of an abstract type, the {collection} is this
96-
list. When the field returns a connection type over an abstract type, the
97-
{collection} is the list of nodes the connection represents.
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.
98191

99192
When a field with a `@limitTypes` argument is being resolved:
100193

@@ -114,5 +207,52 @@ each entry in {collection} is a type within {allowedTypes}. The resolver must
114207
apply this restriction before applying any pagination arguments.
115208

116209
Note: The restriction must be applied before pagination arguments so that
117-
non-terminal pages in the collection get full representation - i.e. there are no
118-
gaps.
210+
non-terminal pages in the {collection} get full representation - i.e. there are
211+
no gaps.
212+
213+
### Field Collection Validation
214+
215+
TODO: the following should raise an error since `Mouse` does not appear as a
216+
value in {allowedTypes}
217+
218+
```graphql counter-example
219+
{
220+
allPets(only: ["Cat", "Dog"]) {
221+
... on Cat { name }
222+
... on Dog { name }
223+
... on Mouse { name }
224+
}
225+
}
226+
```
227+
228+
### Field Response Validation (wip)
229+
230+
TODO: if the response array of the field contains a type that did not appear in
231+
{CoerceAllowedTypes()}, raise an execution error<br><br>
232+
yes, if a resolver already correctly implements the "Enforcing Allowed Types"
233+
logic then this isn't necessary - but - I think this is worth speccing out as a
234+
dedicated step because this is likely something tooling will want to be able to
235+
automatically apply to all @limitTypes'd fields as a middleware. This is to
236+
provide an extra layer of safety (otherwise we're trusting that human
237+
implementers got it right inside the resolver)
238+
239+
For example, given a *filter argument* of `["Cat", "Dog"]`, the following would
240+
be invalid since {allPets} contains `Mouse`:
241+
242+
```json counter-example
243+
{
244+
"data": {
245+
"allPets": [
246+
{ "__typename": "Cat", "name": "Tom" },
247+
{ "__typename": "Mouse", "name": "Jerry" }
248+
]
249+
}
250+
}
251+
```
252+
253+
...is this even possible? this assumes that client asks for `__typename`
254+
which isn't guaranteed. https://spec.graphql.org/draft/#ResolveAbstractType()
255+
likely is not possible since this logic is intended to be run generically as a
256+
middleware - i.e _after_ the field has completed, and the in-memory object
257+
representation has been converted into json blob (potentially without
258+
`__typename`)

rfcs/AbstractFilter/Index.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# GraphQL Abstract Type Filter Specification
2+
3+
_Status: Strawman_
4+
5+
This specification aims to provide a standardized way for clients to communicate
6+
the exclusive set of types permitted in a resolver’s response when returning one
7+
or more abstract types (i.e. an Interface or Union return type).
8+
9+
Algorithms are provided for resolvers to enforce this contract at runtime.
10+
11+
In the following example, `getMedia` will return **only** `Book` or `Movie`
12+
types:
13+
14+
```graphql example
15+
query GetMedia {
16+
getMedia(only: ["Book", "Movie"]) {
17+
... on Book { author }
18+
... on Movie { director }
19+
}
20+
}
21+
```
22+
23+
This is enforced on the server when using the `@limitTypes` type system
24+
directive:
25+
26+
```graphql example
27+
union Media = Book | Movie | Opera
28+
29+
type Query {
30+
getMedia(only: [String] @limitTypes): Media
31+
}
32+
```
33+
34+
**@matches**
35+
36+
This document also specifies a `@matches` executable directive. Client tooling
37+
may implement this to let query authors avoid manually defining the allowed
38+
types (which is implicitly already defined inside the
39+
[selection set](<https://spec.graphql.org/draft/#sec-Selection-Sets>) of the
40+
{field} for which the {argument} the directive is applied to).
41+
42+
The following example is identical to the query above when compiled (either at
43+
build time, or as a runtime transformation):
44+
45+
```graphql example
46+
query GetMedia {
47+
getMedia @matches {
48+
... on Book { title author }
49+
... on Movie { title director }
50+
}
51+
}
52+
```
53+
54+
**Use Cases**
55+
56+
Applications may implement this specification to provide a filter for what
57+
type(s) may be returned by a resolver. Notably, the filtering happens on the
58+
server side allowing clients to receive a fixed length of results.
59+
60+
This may also be used a versioning scheme by applications that dynamically
61+
render different parts of a user interface mapped from the return type(s) of a
62+
resolver. Each version of the application can define the exclusive set of types
63+
it supports displaying in the user interface.
64+
65+
# [AbstractFilterArgumentSpec](AbstractFilterArgumentSpec.md)
66+
# [Matches](Matches.md)
67+

0 commit comments

Comments
 (0)