Skip to content

Commit 476ee42

Browse files
committed
GAP-0001: Add Abstract Type Filters Spec
1 parent be0d18e commit 476ee42

5 files changed

Lines changed: 752 additions & 0 deletions

File tree

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
# Abstract Type Filter Argument
2+
3+
## @limitTypes
4+
5+
```graphql
6+
directive @limitTypes on ARGUMENT_DEFINITION
7+
```
8+
9+
`@limitTypes` is a type system directive that may be applied to a field
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.
15+
16+
**Example Usage**
17+
18+
```graphql example
19+
type Query {
20+
allPets(only: [String] @limitTypes): [Pet]
21+
}
22+
23+
interface Pet {
24+
name: String!
25+
}
26+
27+
type Cat implements Pet {
28+
name: String!
29+
}
30+
31+
type Dog implements Pet {
32+
name: String!
33+
}
34+
```
35+
36+
`@limitTypes` may also be applied to schema that implements the
37+
[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm#sec-Connection-Types):
38+
39+
```graphql example
40+
type Query {
41+
allPetsConnection(
42+
first: Int
43+
after: String
44+
only: [String] @limitTypes
45+
): PetConnection
46+
}
47+
48+
type PetConnection {
49+
edges: [PetEdge]
50+
pageInfo: PageInfo!
51+
}
52+
53+
type PetEdge {
54+
cursor: String!
55+
node: Pet
56+
}
57+
```
58+
59+
## Schema Validation
60+
61+
The `@limitTypes` directive must not appear on more than one argument on the
62+
same field.
63+
64+
The `@limitTypes` directive may only appear on an argument that accepts a
65+
(possibly non-nullable) list of (possibly non-nullable) String.
66+
67+
The `@limitTypes` directive may only appear on an field argument where the field
68+
returns either:
69+
70+
- an abstract type
71+
- a list of 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)
75+
76+
## Execution
77+
78+
The `@limitTypes` directive places requirements on the {resolver} used to
79+
satisfy the field. Implementers of this specification must honor these
80+
requirements.
81+
82+
### Coercing Allowed Types
83+
84+
:: A *filter argument* is a field argument which has the `@limitTypes`
85+
directive applied.
86+
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.
91+
92+
:: The coerced list of valid concrete object types are the *allowed types*.
93+
94+
CoerceAllowedTypes(abstractType, typeNames):
95+
96+
- Let {possibleTypes} be a set of the possible types of {abstractType}.
97+
- Let {allowedTypes} be an empty unordered set of object types.
98+
- For each {typeName} in {typeNames}:
99+
- Let {type} be the type in the schema named {typeName}.
100+
- If {type} does not exist, raise an execution error.
101+
- If {type} is an object type:
102+
- If {type} is a member of {possibleTypes}, add {type} to {allowedTypes}.
103+
- Otherwise, raise an execution error.
104+
- Otherwise, if {type} is a union type:
105+
- For each {concreteType} in {type}:
106+
- If {concreteType} is a member of {possibleTypes}, add {concreteType} to
107+
{allowedTypes}.
108+
- Otherwise, if {type} is an interface type:
109+
- For each {concreteType} that implements {type}:
110+
- If {concreteType} is a member of {possibleTypes}, add {concreteType} to
111+
{allowedTypes}.
112+
- Otherwise, raise an execution error (scalars, enums, and input types are not
113+
valid filter argument values).
114+
- Return {allowedTypes}.
115+
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+
```
139+
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
170+
[`ResolveFieldValue()`](<https://spec.graphql.org/draft/#ResolveFieldValue()>)
171+
during the [`ExecuteField()`](<https://spec.graphql.org/draft/#ExecuteField()>)
172+
algorithm.
173+
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.
182+
183+
When a field with a `@limitTypes` argument is being resolved:
184+
185+
- Let {limitTypesArgument} be the first argument with the `@limitTypes`
186+
directive.
187+
- If no such argument exists, no further action is necessary.
188+
- If {argumentValues} does not provide a value for {limitTypesArgument}, no
189+
further action is necessary.
190+
- Let {limitTypes} be the value in {argumentValues} of {limitTypesArgument}.
191+
- If {limitTypes} is {null}, no further action is necessary.
192+
- Let {abstractType} be the abstract type the {collection} represents.
193+
- Let {allowedTypes} be {CoerceAllowedTypes(abstractType, limitTypes)}.
194+
195+
Note: The restriction must be applied before pagination arguments so that
196+
non-terminal pages in the collection get full representation - i.e. there
197+
are no gaps.
198+
199+
## Validation Algorithms
200+
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.
205+
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 member type of {collection}.
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 the execution of the resolver
234+
provided by the application code.
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+
- Let {hasValidMember} be {false}.
247+
- For each {concreteType} in {type}:
248+
- If {concreteType} is a member of {possibleTypes}, let {hasValidMember}
249+
be {true}.
250+
- If {hasValidMember} is {false}, raise an execution error.
251+
- Otherwise, if {type} is an interface type:
252+
- Let {hasValidMember} be {false}.
253+
- For each {concreteType} that implements {type}:
254+
- If {concreteType} is a member of {possibleTypes}, let {hasValidMember}
255+
be {true}.
256+
- If {hasValidMember} is {false}, raise an execution error.
257+
- Otherwise, raise an execution error (scalars, enums, and input types are not
258+
valid filter argument values).
259+
260+
Note: Schema-aware clients or linting tools are encouraged to implement this
261+
validation locally.
262+
263+
### Field Collection Validation (wip)
264+
265+
For example, the following query must raise an execution error since `Mouse`
266+
does not appear as a value in {allowedTypes}
267+
268+
```graphql counter-example
269+
{
270+
allPets(only: ["Cat", "Dog"]) {
271+
... on Cat { name }
272+
... on Dog { name }
273+
... on Mouse { name }
274+
}
275+
}
276+
```
277+
278+
TODO: implement algorithm
279+
280+
### Field Response Validation (wip)
281+
282+
TODO: if the response array of the field contains a type that did not appear in
283+
{CoerceAllowedTypes()}, raise an execution error<br><br>
284+
yes, if a resolver already correctly implements the "Enforcing Allowed Types"
285+
logic then this isn't necessary - but - I think this is worth speccing out as a
286+
dedicated step because this is likely something tooling will want to be able to
287+
automatically apply to all @limitTypes'd fields as a middleware. This is to
288+
provide an extra layer of safety (otherwise we're trusting that human
289+
implementers got it right inside the resolver)
290+
291+
For example, given a *filter argument* of `["Cat", "Dog"]`, the following would
292+
be invalid since {allPets} contains `Mouse`:
293+
294+
```json counter-example
295+
{
296+
"data": {
297+
"allPets": [
298+
{ "__typename": "Cat", "name": "Tom" },
299+
{ "__typename": "Mouse", "name": "Jerry" }
300+
]
301+
}
302+
}
303+
```
304+
305+
...is this even possible? this assumes that client asks for `__typename`
306+
which isn't guaranteed. https://spec.graphql.org/draft/#ResolveAbstractType()
307+
likely is not possible since this logic is intended to be run generically as a
308+
middleware - i.e _after_ the field has completed, and the in-memory object
309+
representation has been converted into json blob (potentially without
310+
`__typename`)
311+
312+
or can we look at using \_\_resolveType()?

GAP-0001/DRAFT/Index.md

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

0 commit comments

Comments
 (0)