Skip to content
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

Fixed ability to skip interface implementations and union cases in query #458

Merged
merged 3 commits into from
Feb 15, 2024
Merged
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
19 changes: 12 additions & 7 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,7 @@ 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, obj()) |> ResolverResult.data |> AsyncVal.wrap

| Union uDef ->
let possibleTypesFn = ctx.Schema.GetPossibleTypes
Expand All @@ -382,7 +387,7 @@ 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, obj()) |> ResolverResult.data |> AsyncVal.wrap

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

Expand All @@ -393,7 +398,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 +449,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, box [])) 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 Expand Up @@ -510,12 +515,12 @@ and private executeResolvers (ctx : ResolveFieldContext) (path : FieldPath) (par
match info.Kind, returnDef with
| ResolveDeferred innerInfo, _ when innerInfo.IsNullable -> // We can only defer nullable fields
deferred
|> resolveWith { ctx with ExecutionInfo = { innerInfo with IsNullable = false } }
|> resolveWith { ctx with ExecutionInfo = innerInfo }
xperiandri marked this conversation as resolved.
Show resolved Hide resolved
| ResolveDeferred innerInfo, _ ->
raiseErrors <| deferredNullableError (innerInfo.Identifier) (innerInfo.ReturnDef.ToString()) path ctx
| ResolveStreamed (innerInfo, mode), HasList innerDef -> // We can only stream lists
streamed mode innerDef
|> resolveWith { ctx with ExecutionInfo = innerInfo; }
|> resolveWith { ctx with ExecutionInfo = innerInfo }
| ResolveStreamed (innerInfo, _), _ ->
raiseErrors <| streamListError innerInfo.Identifier (returnDef.ToString()) path ctx
| ResolveLive innerInfo, _ ->
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
106 changes: 105 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,7 @@ 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 } ]
) ]
),
config = { SchemaConfig.Default with Types = [ CatType; DogType ] }
Expand Down Expand Up @@ -111,6 +111,48 @@ let ``Execute handles execution of abstract types: isTypeOf is used to resolve r
empty errors
data |> equals (upcast expected)

[<Fact>]
let ``Execute handles execution of abstract types: not specified Interface types must be empty objects`` () =
let query =
"""{
pets {
... on Dog {
name
woofs
}
}
}"""

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

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

ensureDirect result <| fun data errors ->
empty errors
let [| dog; emptyObj |] = data["pets"] :?> obj array
dog |> equals (upcast expected)
emptyObj.GetType() |> equals typeof<obj>

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

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

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

ensureDirect result <| fun data errors ->
empty errors
let [| emptyObj; cat|] = data["pets"] :?> obj array
cat |> equals (upcast expected)
emptyObj.GetType() |> equals typeof<obj>

[<Fact>]
let ``Execute handles execution of abstract types: absent field resolution produces errors for Interface`` () =
let query =
Expand Down Expand Up @@ -155,6 +197,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 @@ -219,6 +281,48 @@ let ``Execute handles execution of abstract types: isTypeOf is used to resolve r
empty errors
data |> equals (upcast expected)

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

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

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

ensureDirect result <| fun data errors ->
empty errors
let [| dog; emptyObj |] = data["pets"] :?> obj array
dog |> equals (upcast expected)
emptyObj.GetType() |> equals typeof<obj>

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

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

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

ensureDirect result <| fun data errors ->
empty errors
let [| emptyObj; cat|] = data["pets"] :?> obj array
cat |> equals (upcast expected)
emptyObj.GetType() |> equals typeof<obj>

[<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
Loading