|
| 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()? |
0 commit comments