-
Notifications
You must be signed in to change notification settings - Fork 242
Abstract filter argument #1874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Abstract filter argument #1874
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
4b27686
Write up thoughts based on results of discussion
benjie b128548
Update rfcs/AbstractFilter/AbstractFilterArgumentSpec.md
benjie 9352348
Update rfcs/AbstractFilter/AbstractFilterArgumentSpec.md
benjie 6721895
Update rfcs/AbstractFilter/README.md
benjie File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| # Abstract Filter Argument | ||
|
|
||
| Status: Strawman | ||
|
|
||
| ## Directive | ||
|
|
||
| ```graphql | ||
| directive @limitTypes on FIELD_DEFINITION | ||
| ``` | ||
|
|
||
| ### Examples | ||
|
|
||
| ```graphql | ||
| type Query { | ||
| allPets(first: Int, only: [String] @limitTypes): [Pet] | ||
| } | ||
|
|
||
| interface Pet { | ||
| name: String! | ||
| } | ||
| ``` | ||
|
|
||
| ```graphql | ||
| type Query { | ||
| allPetsConnection( | ||
| first: Int | ||
| after: String | ||
| only: [String] @limitTypes | ||
| ): PetConnection | ||
| } | ||
|
|
||
| # TODO: connection types | ||
|
|
||
| interface Pet { | ||
| name: String! | ||
| } | ||
| ``` | ||
|
|
||
| ## Schema Validation | ||
|
|
||
| The `@limitTypes` directive must not appear on more than one argument on the | ||
| same field. | ||
|
|
||
| The `@limitTypes` directive may only appear on an argument that accepts a | ||
| (possibly non-nullable) list of (possibly non-nullable) String. | ||
|
|
||
| The `@limitTypes` directive may only appear on an argument to a field whose | ||
| named return type is a connection type (that is to say, conforming to | ||
| [the GraphQL Cursor Connections Specification's Connection Type](https://relay.dev/graphql/connections.htm#sec-Connection-Types)) | ||
| over an abstract type, or to a field whose return type is a list and the named | ||
| return type is an abstract type. | ||
|
|
||
| ## Execution | ||
|
|
||
| The `@limitTypes` directive places requirements on the {resolver} used to | ||
| satisfy the field. Implementers of this specification must honour these | ||
| requirements. | ||
|
|
||
| ### Coerce Allowed Types | ||
|
|
||
| The input to the filter argument is a list of strings, however this must be made | ||
| meaningful to the resolver such that it may perform its filtering - thus we must | ||
| resolve it into a list of valid concrete object types that are possible in this | ||
| position. | ||
|
|
||
| CoerceAllowedTypes(abstractType, typeNames): | ||
|
|
||
| - Let {possibleTypes} be a set of the possible types of {abstractType}. | ||
| - Let {allowedTypes} be an empty unordered set of object types. | ||
| - For each {typeName} in {typeNames}: | ||
| - Let {type} be the type in the schema named {typeName}. | ||
| - If {type} does not exist, continue to the next {typeName}. | ||
| - If {type} is an object type: | ||
| - If {type} is a member of {possibleTypes}, add {type} to {allowedTypes}. | ||
| - Otherwise, if {type} is a union type: | ||
| - For each {concreteType} in {type}: | ||
| - If {concreteType} is a member of {possibleTypes}, add {concreteType} to | ||
| {allowedTypes}. | ||
| - Otherwise, if {type} is an interface type: | ||
| - For each {concreteType} that implements {type}: | ||
| - If {concreteType} is a member of {possibleTypes}, add {concreteType} to | ||
| {allowedTypes}. | ||
| - Otherwise continue to the next {typeName}. | ||
| - Return {allowedTypes}. | ||
|
|
||
| ### Enforcing Allowed Types | ||
|
|
||
| Enforcement of the allowed types is the responsibility of the {resolver} called | ||
| in | ||
| [`ResolveFieldValue()`](<https://spec.graphql.org/draft/#ResolveFieldValue()>) | ||
| during the [`ExecuteField()`](<https://spec.graphql.org/draft/#ExecuteField()>) | ||
| algorithm. This is because the filtering must be applied to the {collection} | ||
| prior to applying the pagination arguments. | ||
|
|
||
| When the field returns a list of an abstract type, the {collection} is this | ||
| list. When the field returns a connection type over an abstract type, the | ||
| {collection} is the list of nodes the connection represents. | ||
|
|
||
| When a field with a `@limitTypes` argument is being resolved: | ||
|
|
||
| - Let {limitTypesArgument} be the first argument with the `@limitTypes` | ||
| directive. | ||
| - If no such argument exists, no further action is necessary. | ||
| - If {argumentValues} does not provide a value for {limitTypesArgument}, no | ||
| further action is necessary. | ||
| - Let {limitTypes} be the value in {argumentValues} of {limitTypesArgument}. | ||
| - If {limitTypes} is {null}, no further action is necessary. | ||
| - Let {abstractType} be the abstract type the {collection} represents. | ||
| - Let {allowedTypes} be {CoerceAllowedTypes(abstractType, limitTypes)}. | ||
|
|
||
| The resolver must ensure that the result of | ||
| [`ResolveAbstractType()`](<https://spec.graphql.org/draft/#ExecuteField()>) for | ||
| each entry in {collection} is a type within {allowedTypes}. The resolver must | ||
| apply this restriction before applying any pagination arguments. | ||
|
|
||
| Note: The restriction must be applied before pagination arguments so that | ||
| non-terminal pages in the collection get full representation - i.e. there are no | ||
| gaps. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| TODO: copy over matches spec, and update based on discussion and link to | ||
| AbstractFilterArgumentSpec |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| # Abstract Filter | ||
|
|
||
| Champion: Mark Larah (@magicmark) | ||
|
|
||
| ## Overview | ||
|
|
||
| A schema may offer a field that returns a list of an abstract type (an interface | ||
| or union), but the user may only want a subset of these types to be returned, or | ||
| the client may only support a subset of these types (or only current versions of | ||
| the types, not those that are added to the union or implement the interface in | ||
| the future). Typically, the user's preference will be that that filtering take | ||
| place _before_ pagination arguments are applied. | ||
|
|
||
| For example, a user is browsing a pet store, but is only interested in cats and | ||
| fish, they want dogs to be excluded: | ||
|
|
||
| ```graphql | ||
| { | ||
| allPets(first: 5, only: ["Cat", "Fish"]) { | ||
| name | ||
| price | ||
| ... on Cat { | ||
| breed | ||
| } | ||
| ... on Fish { | ||
| species | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Now consider widget-based user interfaces, where each concrete type has its own | ||
| widget. This is common in server-driven UI (SDUI); for example: | ||
|
|
||
| ```graphql | ||
| { | ||
| newsFeed(first: 5) { | ||
| ...StatusFragment | ||
| ...PhotoFragment | ||
| ...EventFragment | ||
| } | ||
| } | ||
| fragment StatusFragment on Status { | ||
| author { | ||
| name | ||
| avatar | ||
| } | ||
| text | ||
| comments(first: 2) { | ||
| author { | ||
| name | ||
| avatar | ||
| } | ||
| text | ||
| } | ||
| } | ||
| fragment PhotoFragment on Photo { | ||
| url | ||
| tags { | ||
| user { | ||
| name | ||
| avatar | ||
| } | ||
| text | ||
| } | ||
| comments(first: 2) { | ||
| author { | ||
| name | ||
| avatar | ||
| } | ||
| text | ||
| } | ||
| } | ||
| fragment EventFragment on Event { | ||
| title | ||
| startTime | ||
| duration | ||
| description | ||
| location | ||
| entryFee | ||
| } | ||
| ``` | ||
|
|
||
| As the schema evolves, `newsFeed` might add a new type to the union: `Video`. | ||
| The existing deployed client now receives empty objects where a video would be, | ||
| and the UI no longer renders 5 tiles reliably. The client would like to filter | ||
| to only the types it supports: | ||
|
|
||
| ```graphql | ||
| { | ||
| newsFeed(first: 5, only: ["Status", "Photo", "Event"]) { | ||
| ...StatusFragment | ||
| ...PhotoFragment | ||
| ...EventFragment | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| This RFC comes in two parts: | ||
|
|
||
| 1. An `ARGUMENT_DEFINITION` directive and associated behaviors that indicates | ||
| that an argument will ensure only the given types are returned | ||
| 2. A client-only `FIELD` directive that will cause this argument to be | ||
| automatically populated based on the types of fragments used in this field's | ||
| selection set | ||
|
|
||
| ## Decisions | ||
|
|
||
| Here's a rough log of the things that we've determined so far. | ||
|
|
||
| ### String, not enum | ||
|
|
||
| It was proposed that the `only` argument could accept an array of enum values, | ||
| where the enum contained a value for each type the abstract type supported. | ||
|
|
||
| Pros: | ||
|
|
||
| - Could be auto-detected by convention (field returns list of (or connection of) | ||
| abstract type, argument name is `only`, argument is list of enum type, enum | ||
| enum contains a value for each of the possible types of the abstract type and | ||
| nothing else) - no need for spec changes | ||
| - Auto-complete in GraphiQL | ||
| - Automatically validated with errors surfaced with existing tooling already | ||
| - Looks neat and obvious, less visual noise: | ||
| `{ newsFeed(only: [Status, Photo, Event]) {...} }` | ||
|
|
||
| Cons: | ||
|
|
||
| - Would add potentially long enums to schema (noisy) | ||
| - Keeping enums in sync would lean towards some kind of generation in SDL-first | ||
| (e.g. `enum PetType @possibleTypes(typeName: "Pet")`) or dynamic construction | ||
| in code-first. | ||
| - Using the type name verbatim (`GuineaPig`) would break the `UPPER_CAMEL_CASE` | ||
| convention, causing lint failures in some schemas | ||
|
|
||
| The clincher that ruled out the enum type was the "Argument must accept both | ||
| abstract and concrete types" decision - an enum composed of all of the possible | ||
| types of an abstract type **plus** all of the abstract types that those types | ||
| implement or are a member of would be exceedingly verbose. | ||
|
|
||
| ### Argument must accept both abstract and concrete types | ||
|
|
||
| A "know-nothing" client (one that does not know the schema definition) would not | ||
| know whether a fragment spread was on a concrete or abstract type, so the | ||
| argument should support both concrete and abstract types to avoid developer | ||
| pain. | ||
|
|
||
| ### No custom syntax | ||
|
|
||
| Various custom syntaxes were proposed, such that the set of possible types could | ||
| be automatically determined; for example the double-brace syntax: | ||
|
|
||
| ```graphql | ||
| { | ||
| allPets {{ # e.g. resolver passed special argument `__only: ["Cat". "Dog"]` | ||
| Cat { ... CatFragment } | ||
| Dog { ... DogFragment } | ||
| }} | ||
| } | ||
| ``` | ||
|
|
||
| This list of possible types would be used by the client as part of the cache key | ||
| in the normalized store, and also would be fed to the server's resolver to | ||
| ensure only the compatible types were returned (and GraphQL would throw an error | ||
| if this promise were broken). | ||
|
|
||
| All of these fell down when considering indirect relations between the selection | ||
| set and the argument position - how would the client/schema know to feed this | ||
| into a connection? | ||
|
|
||
| ```graphql | ||
| { | ||
| allPetsConnection { # How would we know to pass the special argument here?! | ||
| edges { | ||
| cursor | ||
| node {{ | ||
| Cat { ... CatFragment } | ||
| Dog { ... DogFragment } | ||
| }} | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| It's essential that the client know that this filtering is applied such that its | ||
| normalized store does not become corrupted, and thus it was agreed that we | ||
| should stick with passing the list of allowed types as an argument (since | ||
| arguments already factor into npormalized cache keys). | ||
benjie marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ### Two specifications | ||
|
|
||
| Originally this was considered as a single feature for SDUI usage; however it | ||
| was realised that the filter argument (and associated behavior) was useful even | ||
| without the auto-generation of the value - the definition of behavior of such an | ||
| argument would allow auto-completion in editors, and would be behavior the | ||
| client could rely upon even without requiring the query be formed in a | ||
| particular format. | ||
|
|
||
| ### Directive, not convention | ||
|
|
||
| Clients relying on this functionality need strong guarantees. The GraphQL Cursor | ||
| Connections Specification can rely on conventions since the requirements are | ||
| sufficiently complex that we can be pretty sure someone is deliberately | ||
| implementing that pattern. However, with this proposal, short of using an enum | ||
| type (ruled out above), there's insufficient information to give us the | ||
| confidence that the argument definitely applies the behaviors we expect. | ||
|
|
||
| Given the above, the argument needs an annotation to indicate its special | ||
| behavior for automated tooling to be able to rely on it, and the way to add that | ||
| annotation currently is via a directive. | ||
|
|
||
| It is recognized that currently this directive will not be available via | ||
| introspection. This part of the spec therefore relies on the resolution of | ||
| [#300](https://github.com/graphql/graphql-spec/issues/300). | ||
|
|
||
| ## Prior art | ||
|
|
||
| Relay `@match` directive: | ||
| https://relay.dev/docs/guides/data-driven-dependencies/server-3d/#match-design-principles | ||
|
|
||
| PostGraphile `only` argument: | ||
| https://github.com/graphile/crystal/blob/bcf8326bef7930b02c00b67e4ebda22a49e4f5fa/graphile-build/graphile-build-pg/src/plugins/PgPolymorphismOnlyArgumentPlugin.ts#L202-L204 | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.