Skip to content

Commit

Permalink
Fixed ability to skip interface implementations and union cases in query
Browse files Browse the repository at this point in the history
  • Loading branch information
xperiandri committed Feb 14, 2024
1 parent fe8712c commit 92077ca
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 10 deletions.
17 changes: 12 additions & 5 deletions src/FSharp.Data.GraphQL.Server/Execution.fs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,12 @@ let private resolveField (execute: ExecuteField) (ctx: ResolveFieldContext) (par

type ResolverResult<'T> = Result<'T * IObservable<GQLDeferredResponseContent> option * GQLProblemDetails list, GQLProblemDetails list>

[<RequireQualifiedAccess>]
module ResolverResult =

let data data = Ok (data, None, [])
let defered data deferred = Ok (data, Some deferred, [])

let mapValue (f : 'T -> 'U) (r : ResolverResult<'T>) : ResolverResult<'U> =
Result.map(fun (data, deferred, errs) -> (f data, deferred, errs)) r

Expand All @@ -280,7 +285,7 @@ let private unionImplError unionName tyName path ctx = resolverError path ctx (G
let private deferredNullableError name tyName path ctx = resolverError path ctx (GQLMessageException (sprintf "Deferred field %s of type '%s' must be nullable" name tyName))
let private streamListError name tyName path ctx = resolverError path ctx (GQLMessageException (sprintf "Streamed field %s of type '%s' must be list" name tyName))

let private resolved name v : AsyncVal<ResolverResult<KeyValuePair<string, obj>>> = AsyncVal.wrap <| Ok(KeyValuePair(name, box v), None, [])
let private resolved name v : AsyncVal<ResolverResult<KeyValuePair<string, obj>>> = KeyValuePair(name, box v) |> ResolverResult.data |> AsyncVal.wrap

let deferResults path (res : ResolverResult<obj>) : IObservable<GQLDeferredResponseContent> =
let formattedPath = path |> List.rev
Expand Down Expand Up @@ -370,7 +375,8 @@ let rec private direct (returnDef : OutputDef) (ctx : ResolveFieldContext) (path
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
match Map.tryFind resolvedDef.Name typeMap with
| Some fields -> executeObjectFields fields name resolvedDef ctx path value
| None -> raiseErrors <| interfaceImplError iDef.Name resolvedDef.Name path ctx
| None -> KeyValuePair(name, null) |> ResolverResult.data |> AsyncVal.wrap
//| None -> raiseErrors <| interfaceImplError iDef.Name resolvedDef.Name path ctx

| Union uDef ->
let possibleTypesFn = ctx.Schema.GetPossibleTypes
Expand All @@ -382,7 +388,8 @@ let rec private direct (returnDef : OutputDef) (ctx : ResolveFieldContext) (path
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
match Map.tryFind resolvedDef.Name typeMap with
| Some fields -> executeObjectFields fields name resolvedDef ctx path (uDef.ResolveValue value)
| None -> raiseErrors <| unionImplError uDef.Name resolvedDef.Name path ctx
| None -> KeyValuePair(name, null) |> ResolverResult.data |> AsyncVal.wrap
//| None -> raiseErrors <| unionImplError uDef.Name resolvedDef.Name path ctx

| _ -> failwithf "Unexpected value of returnDef: %O" returnDef

Expand All @@ -393,7 +400,7 @@ and deferred (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (valu
executeResolvers ctx path parent (toOption value |> AsyncVal.wrap)
|> Observable.ofAsyncVal
|> Observable.bind(ResolverResult.mapValue(fun d -> d.Value) >> deferResults path)
AsyncVal.wrap <| Ok(KeyValuePair(info.Identifier, null), Some deferred, [])
ResolverResult.defered (KeyValuePair (info.Identifier, null)) deferred |> AsyncVal.wrap

and private streamed (options : BufferedStreamOptions) (innerDef : OutputDef) (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (value : obj) =
let info = ctx.ExecutionInfo
Expand Down Expand Up @@ -444,7 +451,7 @@ and private streamed (options : BufferedStreamOptions) (innerDef : OutputDef) (c
|> Array.mapi resolveItem
|> Observable.ofAsyncValSeq
|> buffer
AsyncVal.wrap <| Ok(KeyValuePair(info.Identifier, box [||]), Some stream, [])
ResolverResult.defered (KeyValuePair (info.Identifier, null)) stream |> AsyncVal.wrap
| _ -> raise <| GQLMessageException (ErrorMessages.expectedEnumerableValue ctx.ExecutionInfo.Identifier (value.GetType()))

and private live (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (value : obj) =
Expand Down
4 changes: 2 additions & 2 deletions src/FSharp.Data.GraphQL.Server/IO.fs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ type GQLExecutionResult =
static member Invalid(documentId, errors, meta) =
GQLExecutionResult.RequestError(documentId, errors, meta)
static member ErrorAsync(documentId, msg : string, meta) =
asyncVal.Return (GQLExecutionResult.Error (documentId, msg, meta))
AsyncVal.wrap (GQLExecutionResult.Error (documentId, msg, meta))
static member ErrorAsync(documentId, error : IGQLError, meta) =
asyncVal.Return (GQLExecutionResult.Error (documentId, error, meta))
AsyncVal.wrap (GQLExecutionResult.Error (documentId, error, meta))

// TODO: Rename to PascalCase
and GQLResponseContent =
Expand Down
178 changes: 177 additions & 1 deletion tests/FSharp.Data.GraphQL.Tests/AbstractionTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ let schemaWithInterface =
[ Define.Field (
"pets",
ListOf PetType,
fun _ _ -> [ { Name = "Odie"; Woofs = true } :> IPet; upcast { Name = "Garfield"; Meows = false } ]
fun _ _ -> [ { Name = "Odie"; Woofs = true } :> IPet; { Name = "Garfield"; Meows = false } ]
)
Define.Field (
"nullablePets",
ListOf (Nullable PetType),
fun _ _ -> [ { Name = "Odie"; Woofs = true } :> IPet |> Some; { Name = "Garfield"; Meows = false } :> IPet |> Some ]
) ]
),
config = { SchemaConfig.Default with Types = [ CatType; DogType ] }
Expand Down Expand Up @@ -111,6 +116,79 @@ let ``Execute handles execution of abstract types: isTypeOf is used to resolve r
empty errors
data |> equals (upcast expected)

[<Fact(Skip = "Not implemented")>]
let ``Execute handles execution of abstract types: not specified Interface types produce error`` () =
let query =
"""{
pets {
... on Dog {
name
woofs
}
}
}"""

let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query)
ensureRequestError result <| fun [ petsError ] ->
petsError |> ensureValidationError "Field 'pets' does not allow nulls and list values." [ "pets"; "0" ]

let query =
"""{
pets {
... on Cat {
name
meows
}
}
}"""

let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query)
ensureRequestError result <| fun [ petsError ] ->
petsError |> ensureValidationError "Field 'pets' does not allow nulls and list values." [ "pets"; "0" ]

[<Fact>]
let ``Execute handles execution of abstract types: not specified Interface types must be filtered out if they allow null`` () =
let query =
"""{
nullablePets {
... on Dog {
name
woofs
}
}
}"""

let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query)

let expected =
NameValueLookup.ofList
[ "nullablePets", upcast [ NameValueLookup.ofList [ "name", "Odie" :> obj; "woofs", upcast true ] :> obj; null ] ]

ensureDirect result <| fun data errors ->
empty errors
data |> equals (upcast expected)

let query =
"""{
nullablePets {
... on Cat {
name
meows
}
}
}"""

let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query)

let expected =
NameValueLookup.ofList
[ "nullablePets",
upcast [ null; NameValueLookup.ofList [ "name", "Garfield" :> obj; "meows", upcast false ] :> obj ] ]

ensureDirect result <| fun data errors ->
empty errors
data |> equals (upcast expected)

[<Fact>]
let ``Execute handles execution of abstract types: absent field resolution produces errors for Interface`` () =
let query =
Expand Down Expand Up @@ -155,6 +233,26 @@ let ``Execute handles execution of abstract types: absent type resolution produc
catError |> ensureValidationError "Field 'unknownField2' is not defined in schema type 'Cat'." [ "pets"; "unknownField2" ]
dogError |> ensureValidationError "Inline fragment has type condition 'UnknownDog', but that type does not exist in the schema." [ "pets" ]

let query =
"""{
pets {
name
... on Dog {
woofs
unknownField1
}
... on UnknownCat {
meows
unknownField2
}
}
}"""

let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query)
ensureRequestError result <| fun [ catError; dogError ] ->
catError |> ensureValidationError "Field 'unknownField1' is not defined in schema type 'Dog'." [ "pets"; "unknownField1" ]
dogError |> ensureValidationError "Inline fragment has type condition 'UnknownCat', but that type does not exist in the schema." [ "pets" ]


let schemaWithUnion =
lazy
Expand Down Expand Up @@ -184,6 +282,11 @@ let schemaWithUnion =
"pets",
ListOf PetType,
fun _ _ -> [ DogCase { Name = "Odie"; Woofs = true }; CatCase { Name = "Garfield"; Meows = false } ]
)
Define.Field (
"nullablePets",
ListOf (Nullable PetType),
fun _ _ -> [ DogCase { Name = "Odie"; Woofs = true } |> Some; CatCase { Name = "Garfield"; Meows = false } |> Some ]
) ]
)
)
Expand Down Expand Up @@ -219,6 +322,79 @@ let ``Execute handles execution of abstract types: isTypeOf is used to resolve r
empty errors
data |> equals (upcast expected)

[<Fact(Skip = "Not implemented")>]
let ``Execute handles execution of abstract types: not specified Union types produce error`` () =
let query =
"""{
pets {
... on Dog {
name
woofs
}
}
}"""

let result = sync <| schemaWithUnion.Value.AsyncExecute (parse query)
ensureRequestError result <| fun [ petsError ] ->
petsError |> ensureValidationError "Field 'pets' does not allow nulls and list values." [ "pets"; "0" ]

let query =
"""{
pets {
... on Cat {
name
meows
}
}
}"""

let result = sync <| schemaWithUnion.Value.AsyncExecute (parse query)
ensureRequestError result <| fun [ petsError ] ->
petsError |> ensureValidationError "Field 'pets' does not allow nulls and list values." [ "pets"; "0" ]

[<Fact>]
let ``Execute handles execution of abstract types: not specified Union types must be filtered out`` () =
let query =
"""{
nullablePets {
... on Dog {
name
woofs
}
}
}"""

let result = sync <| schemaWithUnion.Value.AsyncExecute (parse query)

let expected =
NameValueLookup.ofList
[ "nullablePets", upcast [ NameValueLookup.ofList [ "name", "Odie" :> obj; "woofs", upcast true ] :> obj; null ] ]

ensureDirect result <| fun data errors ->
empty errors
data |> equals (upcast expected)

let query =
"""{
nullablePets {
... on Cat {
name
meows
}
}
}"""

let result = sync <| schemaWithUnion.Value.AsyncExecute (parse query)

let expected =
NameValueLookup.ofList
[ "nullablePets",
upcast [ null; NameValueLookup.ofList [ "name", "Garfield" :> obj; "meows", upcast false ] :> obj ] ]

ensureDirect result <| fun data errors ->
empty errors
data |> equals (upcast expected)

[<Fact>]
let ``Execute handles execution of abstract types: absent field resolution produces errors for Union`` () =
let query =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ let ``AsyncVal computation defines zero value`` () =

[<Fact>]
let ``AsyncVal can be returned from Async computation`` () =
let a = async { return! asyncVal.Return 1 }
let a = async { return! AsyncVal.wrap 1 }
let res = a |> sync
res |> equals 1

[<Fact>]
let ``AsyncVal can be bound inside Async computation`` () =
let a = async {
let! v = asyncVal.Return 1
let! v = AsyncVal.wrap 1
return v }
let res = a |> sync
res |> equals 1
Expand Down

0 comments on commit 92077ca

Please sign in to comment.