Skip to content

Directive chooser middleware #192

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

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,35 @@ let executor = Executor(schema, middlewares)

The `IdentityNameResolver` is optional, though. If no resolver function is provided, this default implementation of is used. Also, notifications to subscribers must be done via `Publish` of `ILiveFieldSubscriptionProvider`, like explained above.

### Directive chooser middleware

This middleware can be used to modify directive behavior on a per request basis, by applying a directive chooser function to each directive present in the original query:

```fsharp
type DirectiveChooser = Directive -> Directive option
```

This function can be applied on the Metadata object of `AsyncExecute` calls. Let's say that we want to ignore a defer directive (and treat it as a direct result) by a `isTrustedUser` condition:

```fsharp
let schema = Schema(query = queryType)

let middlewares = [ Define.DirectiveChooserMiddleware() ]

let executor = Executor(schema, middlewares)

let isTrustedUser = false // Or any calculated boolean value

let chooser (d : Directive) = if not isTrustedUser then None else Some d

let result =
executor.AsyncExecute(
query,
meta = Metadata.WithDirectiveChooser(chooser))
```

Altough chooser is just a simple `Directive -> Directive option` function, one may opt to use helpers to build and transform a chooser using the `DirectiveChooser` module. It does have many common chooser and transformations available, like `fallbackWhen`, `apply`, `merge` or `map`, for example.

### Using extensions to build your own middlewares

You can use extension methods provided by the `FSharp.Data.GraphQL.Shared` package to help building your own middlewares. When making a middleware, often you will need to modify schema definitions to add features to the schema defined by the user code. The `ObjectListFilter` middleware is an example, where all fields that implements lists of a certain type needs to be modified, by accepting an argument called `filter`.
Expand Down
11 changes: 9 additions & 2 deletions src/FSharp.Data.GraphQL.Samples.GiraffeServer/HttpHandlers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ open FSharp.Data.GraphQL.Execution
open System.IO
open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Types
open FSharp.Data.GraphQL.Server.Middlewares

type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult

Expand Down Expand Up @@ -59,18 +60,24 @@ module HttpHandlers =
let body = readStream ctx.Request.Body
let query = body |> tryParse "query"
let variables = body |> tryParse "variables" |> mapString
let buildMetadata fallbackDirectives =
let chooser =
[ DirectiveChooser.fallbackDefer; DirectiveChooser.fallbackStream; DirectiveChooser.fallbackLive ]
|> DirectiveChooser.composeSeq
|> DirectiveChooser.merge (DirectiveChooser.fallbackWhen (fun _ -> fallbackDirectives))
Metadata.WithDirectiveChooser(chooser)
Copy link
Collaborator Author

@ivelten ivelten Nov 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we can use DirectiveChooser module to quickly build and compose choosers. In this example, I'm fallbacking defer, stream and live directives when fallbackDirectives is true - in other words, they will be returned directly.

match query, variables with
| Some query, Some variables ->
printfn "Received query: %s" query
printfn "Received variables: %A" variables
let query = query |> removeSpacesAndNewLines
let result = Schema.executor.AsyncExecute(query, variables = variables, data = Schema.root) |> Async.RunSynchronously
let result = Schema.executor.AsyncExecute(query, variables = variables, data = Schema.root, meta = buildMetadata true) |> Async.RunSynchronously
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can define our choosers on a per request basis, as they are sent into the Metadata object of the AsyncExecute last param.

printfn "Result metadata: %A" result.Metadata
return! okWithStr (json result) next ctx
| Some query, None ->
printfn "Received query: %s" query
let query = query |> removeSpacesAndNewLines
let result = Schema.executor.AsyncExecute(query) |> Async.RunSynchronously
let result = Schema.executor.AsyncExecute(query, meta = buildMetadata true) |> Async.RunSynchronously
printfn "Result metadata: %A" result.Metadata
return! okWithStr (json result) next ctx
| None, _ ->
Expand Down
12 changes: 5 additions & 7 deletions src/FSharp.Data.GraphQL.Samples.GiraffeServer/Schema.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ namespace FSharp.Data.GraphQL.Samples.GiraffeServer
open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Types
open FSharp.Data.GraphQL.Server.Middlewares
open System.Threading
open System.Threading.Tasks
open FSharp.Data.GraphQL.Ast

type Episode =
| NewHope = 1
Expand Down Expand Up @@ -232,7 +229,7 @@ module Schema =
[
Define.Field("id", String, "The id of the planet", fun _ p -> p.Id)
Define.Field("name", Nullable String, "The name of the planet.", fun _ p -> p.Name)
Define.Field("ismoon", Nullable Boolean, "Is that a moon?", fun _ p -> p.IsMoon)
Define.Field("isMoon", Nullable Boolean, "Is that a moon?", fun _ p -> p.IsMoon)
])

and RootType =
Expand Down Expand Up @@ -277,13 +274,13 @@ module Schema =
"setMoon",
Nullable PlanetType,
"Sets a moon status",
[ Define.Input("id", String); Define.Input("ismoon", Boolean) ],
[ Define.Input("id", String); Define.Input("isMoon", Boolean) ],
fun ctx _ ->
getPlanet (ctx.Arg("id"))
|> Option.map (fun x ->
x.SetMoon(Some(ctx.Arg("ismoon"))) |> ignore
x.SetMoon(Some(ctx.Arg("isMoon"))) |> ignore
schemaConfig.SubscriptionProvider.Publish<Planet> "watchMoon" x
schemaConfig.LiveFieldSubscriptionProvider.Publish<Planet> "Planet" "ismoon" x
schemaConfig.LiveFieldSubscriptionProvider.Publish<Planet> "Planet" "isMoon" x
x))])

let schema = Schema(Query, Mutation, Subscription, schemaConfig)
Expand All @@ -292,6 +289,7 @@ module Schema =
[ Define.QueryWeightMiddleware(2.0, true)
Define.ObjectListFilterMiddleware<Human, Character option>(true)
Define.ObjectListFilterMiddleware<Droid, Character option>(true)
Define.DirectiveChooserMiddleware()
Define.LiveQueryMiddleware() ]

let executor = Executor(schema, middlewares)
Expand Down
14 changes: 13 additions & 1 deletion src/FSharp.Data.GraphQL.Server.Middlewares/DefineExtensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,16 @@ module DefineExtensions =
/// </param>
static member LiveQueryMiddleware(?identityName : IdentityNameResolver) : IExecutorMiddleware =
let identityName = defaultArg identityName (fun _ -> "Id")
upcast LiveQueryMiddleware(identityName)
upcast LiveQueryMiddleware(identityName)

/// <summary>
/// Creates a middleware that can be used to apply a choose function to every Directive of the query.
/// Choose function must be provided in the Metadata object of the execution.
/// </summary>
/// <remarks>
/// When defined, this middleware looks for a DirectiveChooser in the Metadata provider to the executor,
/// and apply it to every directive inside the operation. Chooser can transform or even remove directives,
/// making them loose their effect on the query.
/// </remarks>
static member DirectiveChooserMiddleware() : IExecutorMiddleware =
upcast DirectiveChooserMiddleware()
105 changes: 105 additions & 0 deletions src/FSharp.Data.GraphQL.Server.Middlewares/DirectiveChooser.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
namespace FSharp.Data.GraphQL.Server.Middlewares

open FSharp.Data.GraphQL.Ast

/// A function that checks if a directive should be used in the exection of a query, or changed to a new directive.
type DirectiveChooser = Directive -> Directive option

/// Basic operations on DirectiveChoosers.
[<RequireQualifiedAccess>]
module DirectiveChooser =
/// Apply a chooser to a directive.
let apply (directive : Directive) (chooser : DirectiveChooser) =
chooser directive

/// Builds a chooser that, given a Directive x, returns Some x.
let keep : DirectiveChooser =
let chooser = Some
chooser

/// Builds a chooser that, for any Directive, returns None.
let fallback : DirectiveChooser =
let chooser = fun _ -> None
chooser

/// Builds a chooser that, when run, runs actual chooser, and if it returns Some directive x, maps
/// x directive using mapper function to y directive, and return Some y. Otherwise, returns None.
let map (mapper : Directive -> Directive) (actual : DirectiveChooser) : DirectiveChooser =
let chooser = fun directive ->
match actual directive with
| Some d -> mapper d |> keep
| None -> None
chooser

/// Builds a chooser that, given a Directive x, apply the condition filter function to x,
/// and if it returns true, returns Some x. Otherwise, returns None.
let keepWhen (condition : Directive -> bool) : DirectiveChooser =
let chooser = fun directive ->
if condition directive
then keep directive
else fallback directive
chooser

/// Builds a chooser that, given a Directive x, apply the condition filter function to x,
/// and if it returns true, returns None. Otherwise, returns Some x.
let fallbackWhen (condition : Directive -> bool) : DirectiveChooser =
let chooser = fun directive ->
if condition directive
then fallback directive
else keep directive
chooser

/// Builds a chooser that, given a Directive x, if x.Name equals given name, returns None.
/// Otherwise, returns Some x.
let fallbackByName name = fallbackWhen (fun d -> d.Name = name)

/// Builds a chooser that, given a Directive x, if x.Name is 'defer', returns None.
/// Otherwise, returns Some x.
let fallbackDefer = fallbackByName "defer"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought, I'm not sure if we need all of those helpers on the DirectiveChooser module. Can be useful to make readable code, but things can be much simpler if we just apply the chooser function directly.


/// Builds a chooser that, given a Directive x, if x.Name is 'stream', returns None.
/// Otherwise, returns Some x.
let fallbackStream = fallbackByName "stream"

/// Builds a chooser that, given a Directive x, if x.Name is 'live', returns None.
/// Otherwise, returns Some x.
let fallbackLive = fallbackByName "live"

/// Builds a chooser that, when run, runs actual chooser, and if it returns Some directive x,
/// uses that directive to run other chooser and return its result. If actual chooser returns None,
/// returns None.
let compose (other : DirectiveChooser) (actual : DirectiveChooser) : DirectiveChooser =
let chooser = fun directive ->
match actual directive with
| Some d -> other d
| None -> None
chooser

/// Builds a chooser that, when run, runs actual chooser and other chooser: if any of the choosers return
/// None, then returns None. Otherwise, compose actual into other, run the composed chooser, and return its result.
let merge (other : DirectiveChooser) (actual : DirectiveChooser) : DirectiveChooser =
let chooser = fun directive ->
match actual directive, other directive with
|Some _, Some _ -> compose other actual |> apply directive
| _ -> None
chooser

/// Reduces a sequence of choosers into a single chooser, by applying reducer function.
let reduceSeq reducer (choosers : DirectiveChooser seq) =
choosers |> Seq.reduce reducer

/// Reduces a sequence of choosers into a single chooser, by applying the compose function to reduce it.
let composeSeq (choosers : DirectiveChooser seq) : DirectiveChooser =
let chooser = fun directive ->
match Seq.length choosers with
| 0 -> keep directive
| _ -> choosers |> reduceSeq compose |> apply directive
chooser

/// Reduces a sequence of choosers into a single chooser, by applying the DirectiveChooser.merge to reduce it.
let mergeSeq (choosers : DirectiveChooser seq) : DirectiveChooser =
let chooser = fun directive ->
match Seq.length choosers with
| 0 -> keep directive
| _ -> choosers |> reduceSeq merge |> apply directive
chooser
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="ObjectListFilter.fs" />
<Compile Include="DirectiveChooser.fs" />
<Compile Include="TypeSystemExtensions.fs" />
<Compile Include="SchemaDefinitions.fs" />
<Compile Include="MiddlewareDefinitions.fs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Types.Patterns
open FSharp.Data.GraphQL.Types
open FSharp.Data.GraphQL.Execution
open FSharp.Data.GraphQL.Ast

type internal QueryWeightMiddleware(threshold : float, reportToMetadata : bool) =
let middleware (threshold : float) (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLResponse>) =
Expand Down Expand Up @@ -146,4 +147,24 @@ type internal LiveQueryMiddleware(identityNameResolver : IdentityNameResolver) =
interface IExecutorMiddleware with
member __.CompileSchema = Some middleware
member __.PlanOperation = None
member __.ExecuteOperationAsync = None

type internal DirectiveChooserMiddleware() =
let middleware (ctx : PlanningContext) (next : PlanningContext -> ExecutionPlan) =
let chooser = ctx.Metadata.TryFind<DirectiveChooser>("directiveChooser")
let chooseDirectives (chooser : DirectiveChooser) (opdef : OperationDefinition) : OperationDefinition =
let rec selMapper (selectionSet : Selection list) : Selection list =
selectionSet
|> List.map (fun sel ->
match sel with
| Field f -> Field { f with Directives = f.Directives |> List.choose chooser; SelectionSet = selMapper f.SelectionSet }
| FragmentSpread fs -> FragmentSpread { fs with Directives = fs.Directives |> List.choose chooser }
| InlineFragment fd -> InlineFragment { fd with Directives = fd.Directives |> List.choose chooser; SelectionSet = selMapper fd.SelectionSet })
{ opdef with SelectionSet = selMapper opdef.SelectionSet }
match chooser with
| Some chooser -> next { ctx with Operation = chooseDirectives chooser ctx.Operation }
| None -> next ctx
interface IExecutorMiddleware with
member __.CompileSchema = None
member __.PlanOperation = Some middleware
member __.ExecuteOperationAsync = None
21 changes: 19 additions & 2 deletions src/FSharp.Data.GraphQL.Server.Middlewares/TypeSystemExtensions.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace FSharp.Data.GraphQL.Server.Middlewares

open FSharp.Data.GraphQL.Types

/// Contains extensions for the type system.
[<AutoOpen>]
module TypeSystemExtensions =
Expand All @@ -23,4 +23,21 @@ module TypeSystemExtensions =
member this.Filter =
match this.Args.TryFind("filter") with
| Some (:? ObjectListFilter as f) -> Some f
| _ -> None
| _ -> None

type Metadata with
/// <summary>
/// Creates a new instance of the current Metadata, adding a directive chooser function to it.
/// Directive chooser will be used by a DirectiveFallbackMiddleware if configured in the Executor.
/// </summary>
/// <param name="chooser">The directive chooser to be added in the Metadata object.</param>
member this.WithDirectiveChooser(chooser : DirectiveChooser) =
this.Add("directiveChooser", chooser)

/// <summary>
/// Creates a new instance of Metadata, adding a directive chooser function to it.
/// Directive chooser will be used by a DirectiveFallbackMiddleware if configured in the Executor.
/// </summary>
/// <param name="chooser">The directive chooser to be added in the Metadata object.</param>
static member WithDirectiveChooser(chooser : DirectiveChooser) =
Metadata.Empty.WithDirectiveChooser(chooser)
2 changes: 1 addition & 1 deletion src/FSharp.Data.GraphQL.Server/Planning.fs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ let private directiveIncluder (directive: Directive) : Includer =
| None -> raise (GraphQLException (sprintf "Expected 'if' argument of directive '@%s' to have boolean value but got %A" directive.Name other))

let private incl: Includer = fun _ -> true
let private excl: Includer = fun _ -> false
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just removed this function as it is not referenced anywhere in the code.


let private getIncluder (directives: Directive list) parentIncluder : Includer =
directives
|> List.fold (fun acc directive ->
Expand Down
12 changes: 5 additions & 7 deletions src/FSharp.Data.GraphQL.Shared/AsyncVal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ module AsyncVal =
/// Returned array maintain order of values.
/// If the array contains a Failure, then the entire array will not resolve
let collectSequential (values: AsyncVal<'T> []) : AsyncVal<'T []> =
let mapper =
function
| Value v -> v
| _ -> failwith "Expected AsyncVal to be a direct value."
if values.Length = 0 then Value [||]
elif values |> Array.exists isAsync then
Async(async {
Expand All @@ -129,7 +133,7 @@ module AsyncVal =
| Failure f ->
results.[i] <- raise f
return results })
else Value (values |> Array.map (fun (Value v) -> v))
else Value (values |> Array.map mapper)



Expand Down Expand Up @@ -197,12 +201,6 @@ module AsyncExtensions =

/// Computation expression for working on AsyncVals.
let asyncVal = AsyncValBuilder ()

/// Active pattern used for checking if AsyncVal contains immediate value.
let (|Immediate|_|) (x: AsyncVal<'T>) = match x with | Value v -> Some v | _ -> None

/// Active patter used for checking if AsyncVal wraps an Async computation.
let (|Async|_|) (x: AsyncVal<'T>) = match x with | Async a -> Some a | _ -> None

type Microsoft.FSharp.Control.AsyncBuilder with

Expand Down
Loading