Skip to content

Commit 5114273

Browse files
committed
Adding null resolver tree for null fields, sending deferred fields as null fields in direct result
1 parent cfe07d0 commit 5114273

4 files changed

Lines changed: 110 additions & 103 deletions

File tree

src/FSharp.Data.GraphQL.Server/Execution.fs

Lines changed: 95 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -323,102 +323,106 @@ let private propagateError name err = asyncVal { return ResolverError{ Name = na
323323
/// Builds the result tree for a given query
324324
let rec private buildResolverTree (returnDef: OutputDef) (ctx: ResolveFieldContext) (fieldExecuteMap: FieldExecuteMap) (value: obj option) : AsyncVal<ResolverTree> =
325325
let name = ctx.ExecutionInfo.Identifier
326-
match returnDef with
327-
| Object objdef ->
328-
match ctx.ExecutionInfo.Kind with
329-
| SelectFields fields ->
326+
match ctx.ExecutionInfo.Kind with
327+
| ResolveNull ->
328+
asyncVal { return ResolverLeaf { Name = ctx.ExecutionInfo.Identifier; Value = None } }
329+
| _ ->
330+
match returnDef with
331+
| Object objdef ->
332+
match ctx.ExecutionInfo.Kind with
333+
| SelectFields fields ->
334+
match value with
335+
| Some v -> buildObjectFields fields objdef ctx fieldExecuteMap name v
336+
| None ->
337+
if ctx.ExecutionInfo.IsNullable
338+
then asyncVal { return ResolverObjectNode { Name = name; Value = None; Children = [| |] } }
339+
else nullResolverError name
340+
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
341+
| Scalar scalardef ->
342+
let name = ctx.ExecutionInfo.Identifier
343+
let (coerce: obj -> obj option) = scalardef.CoerceValue
344+
asyncVal {
345+
return ResolverLeaf { Name = name; Value = value |> Option.bind(coerce) }
346+
}
347+
| Enum _ ->
348+
let name = ctx.ExecutionInfo.Identifier
349+
asyncVal {
350+
let value' = value |> Option.bind(fun v -> coerceStringValue v |> Option.map(fun v' -> v' :> obj))
351+
return ResolverLeaf { Name = name; Value = value' }
352+
}
353+
| List (Output innerdef) ->
354+
let innerCtx =
355+
match ctx.ExecutionInfo.Kind with
356+
| ResolveCollection innerPlan -> { ctx with ExecutionInfo = innerPlan }
357+
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
358+
let rec build acc (items: obj list) =
359+
match items with
360+
| value::xs ->
361+
if not innerCtx.ExecutionInfo.IsNullable && isNull value
362+
then nullResolverError innerCtx.ExecutionInfo.Identifier
363+
else
364+
asyncVal {
365+
let! tree = buildResolverTree innerdef innerCtx fieldExecuteMap (toOption value)
366+
let! res =
367+
match tree with
368+
| ResolverError e when not innerCtx.ExecutionInfo.IsNullable -> propagateError name e
369+
| t -> build (t::acc) xs
370+
return res
371+
}
372+
| [] -> asyncVal{ return ResolverListNode{ Name = name; Value = value; Children = acc |> List.rev |> List.toArray }}
373+
match value with
374+
| None when not ctx.ExecutionInfo.IsNullable -> nullResolverError name
375+
| None -> asyncVal{ return ResolverListNode{ Name = name; Value = None; Children = [| |]; } }
376+
| ObjectOption (:? System.Collections.IEnumerable as enumerable) ->
377+
enumerable
378+
|> Seq.cast<obj>
379+
|> Seq.toList
380+
|> build []
381+
| _ -> raise <| GraphQLException (sprintf "Expected to have enumerable value in field '%s' but got '%O'" ctx.ExecutionInfo.Identifier (value.GetType()))
382+
| Nullable (Output innerdef) ->
383+
// Stop propagation of null values
384+
buildResolverTree innerdef ctx fieldExecuteMap value
385+
| Interface idef ->
386+
let possibleTypesFn = ctx.Schema.GetPossibleTypes
387+
let resolver = resolveInterfaceType possibleTypesFn idef
388+
let typeMap =
389+
match ctx.ExecutionInfo.Kind with
390+
| ResolveAbstraction typeMap -> typeMap
391+
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
330392
match value with
331-
| Some v -> buildObjectFields fields objdef ctx fieldExecuteMap name v
393+
| Some v ->
394+
let resolvedDef = resolver v
395+
match Map.tryFind resolvedDef.Name typeMap with
396+
| Some fields -> buildObjectFields fields resolvedDef ctx fieldExecuteMap name v
397+
| None -> asyncVal { return ResolverError { Name = name; Message = ctx.Schema.ParseError (GraphQLException (sprintf "GraphQL Interface '%s' is not implemented by the type '%s'" idef.Name resolvedDef.Name)); PathToOrigin = [] } }
332398
| None ->
333399
if ctx.ExecutionInfo.IsNullable
334400
then asyncVal { return ResolverObjectNode { Name = name; Value = None; Children = [| |] } }
335401
else nullResolverError name
336-
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
337-
| Scalar scalardef ->
338-
let name = ctx.ExecutionInfo.Identifier
339-
let (coerce: obj -> obj option) = scalardef.CoerceValue
340-
asyncVal {
341-
return ResolverLeaf { Name = name; Value = value |> Option.bind(coerce) }
342-
}
343-
| Enum _ ->
344-
let name = ctx.ExecutionInfo.Identifier
345-
asyncVal {
346-
let value' = value |> Option.bind(fun v -> coerceStringValue v |> Option.map(fun v' -> v' :> obj))
347-
return ResolverLeaf { Name = name; Value = value' }
348-
}
349-
| List (Output innerdef) ->
350-
let innerCtx =
351-
match ctx.ExecutionInfo.Kind with
352-
| ResolveCollection innerPlan -> { ctx with ExecutionInfo = innerPlan}
353-
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
354-
let rec build acc (items: obj list) =
355-
match items with
356-
| value::xs ->
357-
if not innerCtx.ExecutionInfo.IsNullable && isNull value
358-
then nullResolverError innerCtx.ExecutionInfo.Identifier
359-
else
360-
asyncVal {
361-
let! tree = buildResolverTree innerdef innerCtx fieldExecuteMap (toOption value)
362-
let! res =
363-
match tree with
364-
| ResolverError e when not innerCtx.ExecutionInfo.IsNullable -> propagateError name e
365-
| t -> build (t::acc) xs
366-
return res
367-
}
368-
| [] -> asyncVal{ return ResolverListNode{ Name = name; Value = value; Children = acc |> List.rev |> List.toArray }}
369-
match value with
370-
| None when not ctx.ExecutionInfo.IsNullable -> nullResolverError name
371-
| None -> asyncVal{ return ResolverListNode{ Name = name; Value = None; Children = [| |]; } }
372-
| ObjectOption (:? System.Collections.IEnumerable as enumerable) ->
373-
enumerable
374-
|> Seq.cast<obj>
375-
|> Seq.toList
376-
|> build []
377-
| _ -> raise <| GraphQLException (sprintf "Expected to have enumerable value in field '%s' but got '%O'" ctx.ExecutionInfo.Identifier (value.GetType()))
378-
| Nullable (Output innerdef) ->
379-
// Stop propagation of null values
380-
buildResolverTree innerdef ctx fieldExecuteMap value
381-
| Interface idef ->
382-
let possibleTypesFn = ctx.Schema.GetPossibleTypes
383-
let resolver = resolveInterfaceType possibleTypesFn idef
384-
let typeMap =
385-
match ctx.ExecutionInfo.Kind with
386-
| ResolveAbstraction typeMap -> typeMap
387-
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
388-
match value with
389-
| Some v ->
390-
let resolvedDef = resolver v
391-
match Map.tryFind resolvedDef.Name typeMap with
392-
| Some fields -> buildObjectFields fields resolvedDef ctx fieldExecuteMap name v
393-
| None -> asyncVal { return ResolverError { Name = name; Message = ctx.Schema.ParseError (GraphQLException (sprintf "GraphQL Interface '%s' is not implemented by the type '%s'" idef.Name resolvedDef.Name)); PathToOrigin = [] } }
394-
| None ->
395-
if ctx.ExecutionInfo.IsNullable
396-
then asyncVal { return ResolverObjectNode { Name = name; Value = None; Children = [| |] } }
397-
else nullResolverError name
398-
| Union udef ->
399-
let possibleTypesFn = ctx.Schema.GetPossibleTypes
400-
let resolver = resolveUnionType possibleTypesFn udef
401-
let typeMap =
402-
match ctx.ExecutionInfo.Kind with
403-
| ResolveAbstraction typeMap -> typeMap
404-
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
405-
match value with
406-
| Some v ->
407-
let resolvedDef = resolver v
408-
match Map.tryFind resolvedDef.Name typeMap with
409-
| Some fields ->
410-
// Make sure to propagate the original union type to the object node
411-
buildObjectFields fields resolvedDef ctx fieldExecuteMap name (udef.ResolveValue v)
412-
|> AsyncVal.map(fun tree ->
413-
match tree with
414-
| ResolverObjectNode node -> ResolverObjectNode { node with Value = value }
415-
| t -> t)
416-
| None -> asyncVal { return ResolverError { Name = name; Message = ctx.Schema.ParseError (GraphQLException (sprintf "GraphQL Union '%s' is not implemented by the type '%s'" udef.Name resolvedDef.Name)); PathToOrigin = [] } }
417-
| None ->
418-
if ctx.ExecutionInfo.IsNullable
419-
then asyncVal { return ResolverObjectNode { Name = name; Value = None; Children = [| |] } }
420-
else nullResolverError name
421-
| _ -> failwithf "Unexpected value of returnDef: %O" returnDef
402+
| Union udef ->
403+
let possibleTypesFn = ctx.Schema.GetPossibleTypes
404+
let resolver = resolveUnionType possibleTypesFn udef
405+
let typeMap =
406+
match ctx.ExecutionInfo.Kind with
407+
| ResolveAbstraction typeMap -> typeMap
408+
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
409+
match value with
410+
| Some v ->
411+
let resolvedDef = resolver v
412+
match Map.tryFind resolvedDef.Name typeMap with
413+
| Some fields ->
414+
// Make sure to propagate the original union type to the object node
415+
buildObjectFields fields resolvedDef ctx fieldExecuteMap name (udef.ResolveValue v)
416+
|> AsyncVal.map(fun tree ->
417+
match tree with
418+
| ResolverObjectNode node -> ResolverObjectNode { node with Value = value }
419+
| t -> t)
420+
| None -> asyncVal { return ResolverError { Name = name; Message = ctx.Schema.ParseError (GraphQLException (sprintf "GraphQL Union '%s' is not implemented by the type '%s'" udef.Name resolvedDef.Name)); PathToOrigin = [] } }
421+
| None ->
422+
if ctx.ExecutionInfo.IsNullable
423+
then asyncVal { return ResolverObjectNode { Name = name; Value = None; Children = [| |] } }
424+
else nullResolverError name
425+
| _ -> failwithf "Unexpected value of returnDef: %O" returnDef
422426

423427
and buildObjectFields (fields: ExecutionInfo list) (objdef: ObjectDef) (ctx: ResolveFieldContext) (fieldExecuteMap: FieldExecuteMap) (name: string) (value: obj): AsyncVal<ResolverTree> =
424428
let rec build (acc: ResolverTree list) = function

src/FSharp.Data.GraphQL.Server/Linq.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ let rec private getTracks alreadyFound info =
409409
|> Set.map(fun track -> Direct(track, []))
410410
|> flip Set.difference alreadyFound
411411
match info.Kind with
412-
| ResolveValue -> IR(info, tracks, [])
412+
| ResolveValue | ResolveNull -> IR(info, tracks, [])
413413
| SelectFields fieldInfos -> IR(info, tracks, fieldInfos |> List.map (getTracks (alreadyFound + tracks)))
414414
| ResolveCollection inner -> IR(info, tracks, [ getTracks (alreadyFound + tracks) inner ])
415415
| ResolveAbstraction typeMap ->

src/FSharp.Data.GraphQL.Server/Planning.fs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ let rec private plan (ctx : PlanningContext) (stage : PlanningStage) : PlanningS
199199
| Object _ -> planSelection ctx info.Ast.SelectionSet (info, deferredFields, info.Identifier::path) (ref [])
200200
| Nullable returnDef ->
201201
let inner, deferredFields', path' = plan ctx ({ info with ParentDef = info.ReturnDef; ReturnDef = downcast returnDef }, deferredFields, path)
202-
{ inner with IsNullable = true}, deferredFields', path'
202+
{ inner with IsNullable = true }, deferredFields', path'
203203
| List returnDef ->
204204
// We dont yet know the indicies of our elements so we append a dummy value on
205205
let inner, deferredFields', path' = plan ctx ({ info with ParentDef = info.ReturnDef; ReturnDef = downcast returnDef; Identifier = "__index" }, deferredFields, "__index"::info.Identifier::path)
@@ -214,7 +214,7 @@ and private planSelection (ctx: PlanningContext) (selectionSet: Selection list)
214214
let plannedFields, deferredFields'=
215215
selectionSet
216216
|> List.fold(fun (fields : ExecutionInfo list, deferredFields : DeferredExecutionInfo list) selection ->
217-
//FIXME: includer is not passed along from top level fragments (both inline and spreads)
217+
// FIXME: includer is not passed along from top level fragments (both inline and spreads)
218218
let includer = getIncluder selection.Directives info.Include
219219
let updatedInfo = { info with Include = includer }
220220
match selection with
@@ -227,9 +227,9 @@ and private planSelection (ctx: PlanningContext) (selectionSet: Selection list)
227227
let executionPlan, deferredFields', path' = plan ctx (innerInfo, deferredFields, path)
228228
let addedDeferredFields = deferredFields' |> List.skip deferredFields.Length
229229
match field with
230-
| Deferred -> fields, { Info = { info with Kind = SelectFields [ executionPlan ] }; Path = path'; Kind = DeferredExecution; DeferredFields = addedDeferredFields } :: deferredFields
231-
| Live -> fields, { Info = { info with Kind = SelectFields [ executionPlan ] }; Path = path'; Kind = LiveExecution; DeferredFields = addedDeferredFields } :: deferredFields
232-
| Streamed -> fields, { Info = { info with Kind = SelectFields [ executionPlan ] }; Path = path'; Kind = StreamedExecution; DeferredFields = addedDeferredFields } :: deferredFields
230+
| Deferred -> fields @ [ { executionPlan with Kind = ResolveNull; IsNullable = true } ], { Info = { info with Kind = SelectFields [ executionPlan ] }; Path = path'; Kind = DeferredExecution; DeferredFields = addedDeferredFields } :: deferredFields
231+
| Live -> fields @ [ { executionPlan with Kind = ResolveNull; IsNullable = true } ], { Info = { info with Kind = SelectFields [ executionPlan ] }; Path = path'; Kind = LiveExecution; DeferredFields = addedDeferredFields } :: deferredFields
232+
| Streamed -> fields @ [ { executionPlan with Kind = ResolveNull; IsNullable = true } ], { Info = { info with Kind = SelectFields [ executionPlan ] }; Path = path'; Kind = StreamedExecution; DeferredFields = addedDeferredFields } :: deferredFields
233233
| Planned -> fields @ [ executionPlan ], deferredFields' // unfortunatelly, order matters here
234234
| FragmentSpread spread ->
235235
let spreadName = spread.Name
@@ -239,7 +239,7 @@ and private planSelection (ctx: PlanningContext) (selectionSet: Selection list)
239239
visitedFragments := spreadName::!visitedFragments
240240
match ctx.Document.Definitions |> List.tryFind (function FragmentDefinition f -> f.Name.Value = spreadName | _ -> false) with
241241
| Some (FragmentDefinition fragment) when doesFragmentTypeApply ctx.Schema fragment parentDef ->
242-
// retrieve fragment data just as it was normal selection set
242+
// Retrieve fragment data just as it was normal selection set
243243
// TODO: Check if the path is correctly defined
244244
let fragmentInfo, deferredFields', _ = planSelection ctx fragment.SelectionSet (updatedInfo, deferredFields, path) visitedFragments
245245
let fragmentFields = getSelectionFrag fragmentInfo.Kind
@@ -314,7 +314,7 @@ let private planVariables (schema: ISchema) (operation: OperationDefinition) =
314314
| _ -> raise (MalformedQueryException (sprintf "GraphQL query defined variable '$%s' of type '%s' which is not an input type definition" vname (tdef.ToString()))))
315315

316316
let internal planOperation (ctx: PlanningContext) : ExecutionPlan =
317-
// create artificial plan info to start with
317+
// Create artificial plan info to start with
318318
let rootInfo = {
319319
Identifier = null
320320
Kind = Unchecked.defaultof<ExecutionInfoKind>
@@ -327,7 +327,7 @@ let internal planOperation (ctx: PlanningContext) : ExecutionPlan =
327327
let resolvedInfo, deferredFields, _ = planSelection ctx ctx.Operation.SelectionSet (rootInfo, [], []) (ref [])
328328
let deferredFields' =
329329
deferredFields
330-
|> List.map (fun d -> {d with Path = List.rev d.Path})
330+
|> List.map (fun d -> { d with Path = List.rev d.Path })
331331
let topFields =
332332
match resolvedInfo.Kind with
333333
| SelectFields tf -> tf

src/FSharp.Data.GraphQL.Shared/TypeSystem.fs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,7 @@ and ExecutionInfo =
632632
| _ -> Some info
633633
| head::tail ->
634634
match info.Kind with
635-
| ResolveValue _ -> None
635+
| ResolveValue _ | ResolveNull -> None
636636
| ResolveCollection inner -> path inner segments
637637
| SelectFields fields ->
638638
fields
@@ -658,6 +658,9 @@ and ExecutionInfo =
658658
| ResolveValue ->
659659
pad indent sb
660660
sb.Append("ResolveValue: ").AppendLine(nameAs info) |> ignore
661+
| ResolveNull ->
662+
pad indent sb
663+
sb.Append("ResolveNull: ").AppendLine(nameAs info) |> ignore
661664
| SelectFields fields ->
662665
pad indent sb
663666
sb.Append("SelectFields: ").AppendLine(nameAs info) |> ignore
@@ -683,12 +686,10 @@ and ExecutionInfo =
683686
and ExecutionInfoKind =
684687
/// Reduce scalar or enum to a returned value.
685688
| ResolveValue
686-
687689
/// Reduce result set by selecting provided set of fields,
688690
/// defined inside composite type, current execution info
689691
/// refers to.
690692
| SelectFields of fields : ExecutionInfo list
691-
692693
/// Reduce current field as a collection, applying provided
693694
/// execution info on each of the collection's element.
694695
| ResolveCollection of elementPlan : ExecutionInfo
@@ -697,6 +698,8 @@ and ExecutionInfoKind =
697698
/// field infos depending on what concrete object implementation
698699
/// will be found.
699700
| ResolveAbstraction of typeFields : Map<string, ExecutionInfo list>
701+
/// Reduce current field as a null value.
702+
| ResolveNull
700703

701704
/// Wrapper for a resolve method defined by the user or generated by a runtime.
702705
and Resolve =

0 commit comments

Comments
 (0)