-
Notifications
You must be signed in to change notification settings - Fork 26
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
Directives support, elaborator rework and query algebra simplification #466
Merged
Conversation
This file contains 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
+ Full support of custom query and schema directives. Directives are a major GraphQL feature (schema directives are an important part of typical GraphQL access control schemes) and it would be premature to declare a 1.0 release without support. + Simplify the query algebra. The query algebra has accumulated a number of odd cases. For the most part these are unproblematic, but Rename in particular has proven to be awkward. It manifests in user code as the need to use the PossiblyRenamedSelect extractor in effect handlers, and this is confusing and error prone. + Support threading of state through the query elaborator. This enables, amongst other things, a simpler implementation of CSS-style cascading field arguments (eg. to implement filter criteria which should be inherited by children). Directive support ----------------- Directives can be applied to both the GraphQL schema and to queries. Typically directives will be processed by one or more additional phases during query compilation. Included in the tests are two examples, one demonstrating query directives (that is, directives which are added to the query by the client), and one demonstrating schema directives (that is, directives which are added to the query by the application developer). + Added directives to all applicable operation, query and type elements. + Added full directive location, argument and repetition validation logic. + Schema parser/renderer now has full support for directives. + Renamed Directive to DirectiveDef. The type Directive is now an application of a directive. + Added DirectiveDefs for skip, include and deprecated rather than special casing in introspection and directive processing. + Properly handle directive locations in introspection. + Query minimizer now has full support for directives. + Added query directive test as a demo of custom query directives. + Added a schema directive based access control/authorization test as a demo of custom schema directives. Elaborator rework ----------------- To support the threading of state through the query elaboration process an elaboration monad Elab has been introduced. This, + Provides access to the schema, context, variables and fragments of a query. + Supports the transformation of the children of Select to provide semantics for field arguments. + Supports the addition of contextual data to the resulting query both to drive run time behaviour and to support propagation of context to the elaboration of children. + Supports the addition of queries for additional attributes to the resulting query. + Supports context queries against the query being elaborated. + Supports reporting of errors or warnings during elaboration. Query algebra changes --------------------- + Removed Skipped, UntypedNarrows and Wrap query algebra cases. + Added UntypedFragmentSpread and UntypedInlineFragment query algebra cases primarily to provide a position in the query algebra for directives with the FRAGMENT_SPREAD and INLINE_FRAGMENT location to live. + Removed Rename query algebra case and added alias field to Select, The args and directives fields of Select have been moved to a new UntypedSelect query algebra case. + Count nodes now correspond to values rather than fields and so no longer have a field name and must be nested within a Select. + Added operation name and directives to untyped operation types. + Renamed Query.mapFields to mapFieldsR and introduced a non-Result using mapFields. Type/schema changes ------------------- + The Field.isDeprecated and deprecationReason fields have been removed and redefined in terms of directives. + Schema.{queryType, mutationType, subscriptionType} now correctly handle the case where the declared type is nullable. + Added fieldInfo method to Type. + Removed redundant Type.withField and withUnderlyingField. + Renamed EnumValue to EnumValueDefinition. + Renamed EnumType.value to valueDefinition. EnumType.value new returns an EnumValue. + Added Value.VariableRef. + Collected all value elaboration and validation into object Value. Query parser/compiler changes ----------------------------- + QueryParser.parseText incompatible signature change. Now returns full set of operations and fragments. + QueryParser.parseDocument incompatible signature change. Now returns full set of operations and fragments. + QueryParser.parseXxx removed redundant fragments argument. + QueryParser directive processing generalized from skip/include to general directives. + QueryParser.parseValue logic moved to SchemaParser. + QueryCompiler.compileUntyped renamed to compileOperation. + Fragments are now comiled in the context of each operation where there are more than one. This means that fragments are now correctly evaluated in the scope of any operation specific variable bindings. + The parser now recognises arguments as optional in directive definitions. + The parser now supports directives on operations and variable definitions. Mapping/interpreter changes --------------------------- + Removed redundant QueryExecutor trait. + Renamed Mapping.compileAndRunOne and Mapping.compileAndRunAll to compileAndRun and compileAndRunSubscription respectively. + Removed Mapping.run methods because they don't properly accumulate GraphQL compilation warnings. + Added RootEffect.computeUnit for effectful queries/mutations which don't need to modify the query or any special handling for root cursor creation. + RootEffect.computeCursor and RootStream.computeCursor no longer take the query as a argument. + RootEffect.computeQuery and RootStream.computeQuery have been replaced by computeChild methods which transform only the child of the root query. Uses of computeQuery were the main places where PossiblyRenamedSelect had to be used by end users, because the name and alias of the root query had to be preserved. By only giving the end user the opportunity to compute the child query and having the library deal with the top level, this complication has been removed. + RootStream.computeCursor no longer takes the query as a argument. + QueryInterpreter.run no longer generates the full GraphQL response. This is now done in Mapping.compileAndRun/compileAndRunSubscription, allowing compilation warnings to be incorporated in the GraphQL response. + Various errors now correctly identifed as internal rather than GraphQL errors. Miscellaneous changes --------------------- + Added Result.pure and unit. + Fixes to the order of accumulation of GraphQL problems in Result. + Fixed ap of Parallel[Result]'s Applicative to accumulate problems correctly. + Unnested Context and Env from object Cursor because they are now used in non-cursor scenarios. + Added NullFieldCursor which transforms a cursor such that its children are null. + Added NullCursor which transforms a cursor such that it is null. + Moved QueryMinimizer to its own source file. + Updated tutorial and demo.
Whew, you're not kidding, this is big. I'm going to build this branch and update our code to use it and see how it goes. |
tpolecat
approved these changes
Oct 10, 2023
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a big improvement and it all works fine with the latest changes. 👍🏻
This was referenced Nov 8, 2023
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
This excessively large PR is the result of the collision of three overlapping objectives. Whilst they could have been
split apart, that would have resulted in a non-trivial amount of rework as each incremental change was applied.
The three objectives are,
Full support of custom query and schema directives.
Directives are a major GraphQL feature (schema directives are an important part of typical GraphQL access control
schemes) and it would be premature to declare a 1.0 release without support.
Simplify the query algebra.
The query algebra has accumulated a number of odd cases. For the most part these are unproblematic, but
Rename
inparticular has proven to be awkward. It manifests in user code as the need to use the
PossiblyRenamedSelect
extractor in effect handlers, and this is confusing and error prone.
Support threading of state through the query elaborator.
This enables, amongst other things, a simpler implementation of CSS-style cascading field arguments (eg. to
implement filter criteria which should be inherited by children).
Support for directives means adding directives attributes throughout the schema and at various places in the query
algebra, notably
Select
nodes. The most straightforward way of disposing of theRename
node was to incorporate thealias it carries directly into the
Select
node. The combination of these two changes, along with the invariantsassociated with them, made the current approach taken to the select elaborator likely to be too burdensome for end
users. As a result it seemed sensible to take another look at how select elaboration and rework it in the light of the
desire to also thread state through the process.
Hence the size of this PR.
Directive support
Directives can be applied to both the GraphQL schema and to queries. Typically directives will be processed by one or
more additional phases during query compilation. Included in the tests are two examples, one demonstrating query
directives (that is, directives which are added to the query by the client), and one demonstrating schema directives
(that is, directives which are added to the query by the application developer).
The query directive example adds an
@upperCase
directive which may be applied to query fields which haveString
values,
If clients apply this directive to a
String
field, the result will be upper cased,yields,
whereas,
yields,
This is implemented as an additional phase which is added to the
Mapping
,This phase identifies
Select
nodes with the@upperCase
directive applied then, if the type of the correspondingfield is
String
, it adds a field transform which converts the value to upper case when the compiled query isexecuted.
Note that this logic applies to all fields defined by the schema, without requiring any field-specific logic.
The schema directive example adds a simple access control mechanism along the lines of the one described here.
The schema defines
@authenticated
and@hasRole
directives, along with an enum type which definesUSER
andADMIN
roles. Fields and types with the@authenticated
directive applied are only accessible to authenticatedusers, and fields and types with the
@hasRole(requries: <role>)
directive applied require both authentication andpossession on the specified role.
Here user information is only accessible to authenticated users, and access to e-mail and phone information in
addition requires the ADMIN role. The type level
@authenticate
directive applied toMutation
means that mutatiosare only available to authenticated users.
As with the query directive example, this mechanism is implemented as a compiler phase. We have left it to the service
that Grackle is embedded in to assemble authentication information and provided to the compiler as an
AuthStatus
value in the environment under the
"authStatus"
key. The phase transforms the query as before, this time checkingpermissions at each select. If the permission check is successful the select is left unmodified. If the permission
check is unsuccessful then either the entire query fails or, in the case of the
phone
field ofUser
the permissionviolation generates a warning and the field value is returned as
null
.Additional changes
Directive
toDirectiveDef
. The typeDirective
is now an application of a directive.DirectiveDef
s for skip, include and deprecated rather than special casing in introspection and directiveprocessing.
Elaborator rework
To support the threading of state through the query elaboration process an elaboration monad
Elab
has beenintroduced. This,
Select
to provide semantics for field arguments.propagation of context to the elaboration of children.
The following, taken from the Star Wars demo illustrates the change from the old select elaborator to the new scheme,
The most obvious differences visible here are,
Map[TypeRef, PartialFunction[Select, Query]]
we insteadhave a
PartialFunction[(Type, String, List[Binding]), Elab[Unit]
. This means that instead of user code having tounpack all the contents of a
Select
, including a possible alias and directives, it's handed only the typicallyrelevant components. Moving the
Type
into the argument of the partial function also allows for sharing of logicbetween similar fields of different GraphQL types.
Select
node (which is error prone) theElab
monad allows a function to beapplied to the child only, leaving the fiddly work of reconstructing the resulting
Select
to the library.As well as covering the existing use cases more safely, the new elaborator supports more complex scenarios smoothly.
The cascade demo/test provides an example of CSS-style argument cascades from parent fields to child fields,
Here we want the value of
fooBar
in aFooFilter
argument of afoo
field to be propagated to the value offooBar
in aBarFilter
argument of a childbar
field, and vice versa, recursively. Whilst this was possiblepreviously it required a fairly complex initial pass over the query to propagate the values. Under the new schema this
is a great deal more straightforward,
Here we take advantage of the ability to read values from an environment inherited from parent nodes in the query and
to update values in the environment that is passed to child nodes and the elaboration proceeds.
Query algebra changes
Skipped
,UntypedNarrows
andWrap
query algebra cases.UntypedFragmentSpread
andUntypedInlineFragment
query algebra cases primarily to provide a position in thequery algebra for directives with the
FRAGMENT_SPREAD
andINLINE_FRAGMENT
location to live.Rename
query algebra case and added alias field toSelect
, Theargs
anddirectives
fields ofSelect
have been moved to a newUntypedSelect
query algebra case.Count
nodes now correspond to values rather than fields and so no longer have a field name and must be nestedwithin a
Select
.Query.mapFields
tomapFieldsR
and introduced a non-Result usingmapFields
.Type/schema changes
Field.isDeprecated
anddeprecationReason
fields have been removed and redefined in terms of directives.Schema.{queryType, mutationType, subscriptionType}
now correctly handle the case where the declared type isnullable.
fieldInfo
method toType
.Type.withField
andwithUnderlyingField
.EnumValue
toEnumValueDefinition
.EnumType.value
tovalueDefinition
.EnumType.value
new returns anEnumValue
.Value.VariableRef
.object Value
.Query parser/compiler changes
QueryParser.parseText
incompatible signature change. Now returns full set of operations and fragments.QueryParser.parseDocument
incompatible signature change. Now returns full set of operations and fragments.QueryParser.parseXxx
removed redundantfragments
argument.QueryParser
directive processing generalized fromskip
/include
to general directives.QueryParser.parseValue
logic moved toSchemaParser
.QueryCompiler.compileUntyped
renamed tocompileOperation
.are now correctly evaluated in the scope of any operation specific variable bindings.
Mapping/interpreter changes
QueryExecutor
trait.Mapping.compileAndRunOne
andMapping.compileAndRunAll
tocompileAndRun
andcompileAndRunSubscription
respectively.
Mapping.run
methods because they don't properly accumulate GraphQL compilation warnings.RootEffect.computeUnit
for effectful queries/mutations which don't need to modify the query or any specialhandling for root cursor creation.
RootEffect.computeCursor
andRootStream.computeCursor
no longer take the query as a argument.RootEffect.computeQuery
andRootStream.computeQuery
have been replaced bycomputeChild
methods which transformonly the child of the root query. Uses of
computeQuery
were the main places wherePossiblyRenamedSelect
had tobe used by end users, because the name and alias of the root query had to be preserved. By only giving the end user
the opportunity to compute the child query and having the library deal with the top level, this complication has
been removed.
RootStream.computeCursor
no longer takes the query as a argument.QueryInterpreter.run
no longer generates the full GraphQL response. This is now done inMapping.compileAndRun
/compileAndRunSubscription
, allowing compilation warnings to be incorporated in the GraphQLresponse.
Miscellaneous changes
Result.pure
andunit
.Result
.ap
ofParallel[Result]
'sApplicative
to accumulate problems correctly.Context
andEnv
fromobject Cursor
because they are now used in non-cursor scenarios.NullFieldCursor
which transforms a cursor such that its children are null.NullCursor
which transforms a cursor such that it is null.QueryMinimizer
to its own source file.