From 8cb230d6a7568cb1cdb8ad31edd975373c98ce4b Mon Sep 17 00:00:00 2001 From: Peter Kese Date: Sat, 6 Apr 2024 19:37:53 +0200 Subject: [PATCH 1/4] Add support for generic IDictionary query inputs --- src/FSharp.Data.GraphQL.Server/Values.fs | 29 ++++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index b589f91b8..345b94903 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -105,10 +105,30 @@ let rec internal compileByType | InputObject objDef -> let objtype = objDef.Type - let ctor = ReflectionHelper.matchConstructor objtype (objDef.Fields |> Array.map (fun x -> x.Name)) + let (constructor : obj[] -> obj), (parameterInfos : Reflection.ParameterInfo[]) = + if typeof>.IsAssignableFrom(objtype) then + let parameterInfos = [| + for f in objDef.Fields -> + { new Reflection.ParameterInfo() with + member _.Name = f.Name + member _.ParameterType = f.TypeDef.Type + member _.Attributes = Reflection.ParameterAttributes.Optional + } + |] + let constructor (args:obj[]) = + let o = Activator.CreateInstance(objtype) + let dict = o :?> IDictionary + for fld,arg in Seq.zip objDef.Fields args do + dict.Add(fld.Name, arg) + box o + constructor, parameterInfos + else + let ctor = ReflectionHelper.matchConstructor objtype (objDef.Fields |> Array.map (fun x -> x.Name)) + ctor.Invoke, ctor.GetParameters() + let struct (mapper, nullableMismatchParameters, missingParameters) = - ctor.GetParameters () + parameterInfos |> Array.fold (fun struct (all : ResizeArray<_>, areNullable : HashSet<_>, missing : HashSet<_>) param -> match @@ -188,7 +208,7 @@ let rec internal compileByType let! args = argResults |> splitSeqErrorsList - let instance = ctor.Invoke args + let instance = constructor args do! objDef.Validator instance |> ValidationResult.mapErrors (fun err -> @@ -201,7 +221,6 @@ let rec internal compileByType | true, found -> match found with | :? IReadOnlyDictionary as objectFields -> - let argResults = mapper |> Seq.map (fun struct (field, param) -> result { @@ -217,7 +236,7 @@ let rec internal compileByType let! args = argResults |> splitSeqErrorsList - let instance = ctor.Invoke args + let instance = constructor args do! objDef.Validator instance |> ValidationResult.mapErrors (fun err -> From 8c34550e52072963491d06b288b86d3212309dbc Mon Sep 17 00:00:00 2001 From: Peter Kese Date: Sat, 6 Apr 2024 20:37:37 +0200 Subject: [PATCH 2/4] Skip populating Nullable fields with nulls --- src/FSharp.Data.GraphQL.Server/Values.fs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index 345b94903..550679735 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -112,14 +112,19 @@ let rec internal compileByType { new Reflection.ParameterInfo() with member _.Name = f.Name member _.ParameterType = f.TypeDef.Type - member _.Attributes = Reflection.ParameterAttributes.Optional + member _.Attributes = + match f.TypeDef with + | Nullable _ -> Reflection.ParameterAttributes.Optional + | _ -> Reflection.ParameterAttributes.None } |] let constructor (args:obj[]) = let o = Activator.CreateInstance(objtype) let dict = o :?> IDictionary for fld,arg in Seq.zip objDef.Fields args do - dict.Add(fld.Name, arg) + match arg, fld.TypeDef with + | null, Nullable _ -> () // skip populating Nullable fields with nulls + | _, _ -> dict.Add(fld.Name, arg) box o constructor, parameterInfos else From bed4cc68cbf915853351f9635f42d2f8a55045c1 Mon Sep 17 00:00:00 2001 From: Peter Kese Date: Tue, 9 Apr 2024 19:51:23 +0200 Subject: [PATCH 3/4] Hack around the case where field.ExecuteInput is null --- src/FSharp.Data.GraphQL.Server/Values.fs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index 550679735..c4ea6bfb6 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -205,6 +205,19 @@ let rec internal compileByType | None -> Ok <| wrapOptionalNone param.ParameterType field.TypeDef.Type + | Some input when isNull (box field.ExecuteInput) -> + // hack around the case where field.ExecuteInput is null + let rec extract = function + | NullValue -> null + | IntValue i -> box i + | FloatValue f -> box f + | BooleanValue b -> box b + | StringValue s -> box s + | EnumValue e -> box e + | ListValue l -> box (l |> List.map extract) + | ObjectValue o -> o |> Map.map (fun k v -> extract v) |> box + | VariableName v -> failwithf "Todo: extract variable" + extract input |> Ok | Some prop -> field.ExecuteInput prop variables |> Result.map (normalizeOptional param.ParameterType) From 6adcc7595b8024306f9e82a201c9de9bc30baae1 Mon Sep 17 00:00:00 2001 From: Peter Kese Date: Sun, 27 Oct 2024 21:26:03 +0100 Subject: [PATCH 4/4] Executor: allow overriding AsyncExecute --- src/FSharp.Data.GraphQL.Server/Executor.fs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server/Executor.fs b/src/FSharp.Data.GraphQL.Server/Executor.fs index 11c212675..f95922308 100644 --- a/src/FSharp.Data.GraphQL.Server/Executor.fs +++ b/src/FSharp.Data.GraphQL.Server/Executor.fs @@ -175,6 +175,8 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s new(schema) = Executor(schema, middlewares = Seq.empty) + abstract member AsyncExecute: ExecutionPlan * 'Root option * ImmutableDictionary option -> Async + /// /// Asynchronously executes a provided execution plan. In case of repetitive queries, execution plan may be preprocessed /// and cached using `documentId` as an identifier. @@ -186,7 +188,7 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s /// Execution plan for the operation. /// Optional object provided as a root to all top level field resolvers /// Map of all variable values provided by the client request. - member _.AsyncExecute(executionPlan: ExecutionPlan, ?data: 'Root, ?variables: ImmutableDictionary): Async = + default _.AsyncExecute(executionPlan: ExecutionPlan, ?data: 'Root, ?variables: ImmutableDictionary): Async = execute (executionPlan, data, variables) /// @@ -200,10 +202,10 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s /// Map of all variable values provided by the client request. /// In case when document consists of many operations, this field describes which of them to execute. /// A plain dictionary of metadata that can be used through execution customizations. - member _.AsyncExecute(ast: Document, ?data: 'Root, ?variables: ImmutableDictionary, ?operationName: string, ?meta : Metadata): Async = + member this.AsyncExecute(ast: Document, ?data: 'Root, ?variables: ImmutableDictionary, ?operationName: string, ?meta : Metadata): Async = let meta = defaultArg meta Metadata.Empty match createExecutionPlan (ast, operationName, meta) with - | Ok executionPlan -> execute (executionPlan, data, variables) + | Ok executionPlan -> this.AsyncExecute (executionPlan, data, variables) | Error (documentId, errors) -> async.Return <| GQLExecutionResult.Invalid(documentId, errors, meta) /// @@ -217,11 +219,11 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s /// Map of all variable values provided by the client request. /// In case when document consists of many operations, this field describes which of them to execute. /// A plain dictionary of metadata that can be used through execution customizations. - member _.AsyncExecute(queryOrMutation: string, ?data: 'Root, ?variables: ImmutableDictionary, ?operationName: string, ?meta : Metadata): Async = + member this.AsyncExecute(queryOrMutation: string, ?data: 'Root, ?variables: ImmutableDictionary, ?operationName: string, ?meta : Metadata): Async = let meta = defaultArg meta Metadata.Empty let ast = parse queryOrMutation match createExecutionPlan (ast, operationName, meta) with - | Ok executionPlan -> execute (executionPlan, data, variables) + | Ok executionPlan -> this.AsyncExecute (executionPlan, data, variables) | Error (documentId, errors) -> async.Return <| GQLExecutionResult.Invalid(documentId, errors, meta) /// Creates an execution plan for provided GraphQL document AST without