diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs index d5848c1f..9938878d 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs @@ -93,7 +93,7 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat |> Seq.map (fun x -> match x.Name, x.Value with | "filter", (VariableName variableName) -> Ok (ValueSome (ctx.Variables[variableName] :?> ObjectListFilter)) - | "filter", inlineConstant -> ObjectListFilterType.CoerceInput (InlineConstant inlineConstant) |> Result.map ValueOption.ofObj + | "filter", inlineConstant -> ObjectListFilterType.CoerceInput (InlineConstant inlineConstant) ctx.Variables |> Result.map ValueOption.ofObj | _ -> Ok ValueNone) |> Seq.toList match filterResults |> splitSeqErrorsList with diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs index ad79c67a..7f4e5d06 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs @@ -3,9 +3,6 @@ module FSharp.Data.GraphQL.Server.Middleware.SchemaDefinitions open System -open System.Collections.Generic -open System.Collections.Immutable -open System.Text.Json open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Ast @@ -22,7 +19,7 @@ type private ComparisonOperator = | LessThanOrEqual of string | In of string -let rec private coerceObjectListFilterInput x : Result = +let rec private coerceObjectListFilterInput (variables : Variables) inputValue : Result = let parseFieldCondition (s : string) = let s = s.ToLowerInvariant () @@ -81,17 +78,17 @@ let rec private coerceObjectListFilterInput x : Result build (ValueSome (Or (acc, x))) xs build ValueNone x - let rec mapFilter (name : string, value : InputValue) = + let rec mapFilter (condition : ComparisonOperator) (value : InputValue) = let mapFilters fields = let coerceResults = fields - |> Seq.map coerceObjectListFilterInput + |> Seq.map (coerceObjectListFilterInput variables) |> Seq.toList |> splitSeqErrorsList match coerceResults with | Error errs -> Error errs | Ok coerced -> coerced |> Seq.vchoose id |> Seq.toList |> Ok - match parseFieldCondition name, value with + match condition, value with | Equals "and", ListValue fields -> fields |> mapFilters |> Result.map buildAnd | Equals "or", ListValue fields -> fields |> mapFilters |> Result.map buildOr | Equals "not", ObjectValue value -> @@ -126,76 +123,58 @@ let rec private coerceObjectListFilterInput x : Result splitSeqErrors return ValueSome (ObjectListFilter.In { FieldName = fname; Value = parsedValues |> Array.toList }) } + | condition, VariableName variableName -> + match variables.TryGetValue variableName with + | true, value -> mapFilter condition (value |> InputValue.OfObject) + | false, _ -> Errors.Variables.getVariableNotFoundError variableName | _ -> Ok ValueNone and mapInput value = let filterResults = value - |> Map.toSeq - |> Seq.map mapFilter + |> Seq.map (fun kvp -> mapFilter (parseFieldCondition kvp.Key) kvp.Value) |> Seq.toList |> splitSeqErrorsList match filterResults with | Error errs -> Error errs | Ok filters -> filters |> Seq.vchoose id |> List.ofSeq |> buildAnd |> Ok - match x with - | ObjectValue x -> mapInput x - | NullValue -> ValueNone |> Ok - // TODO: Get union case - | _ -> - Error [ - { new IGQLError with - member _.Message = $"'ObjectListFilter' must be defined as object but got '{x.GetType ()}'" - } - ] + let rec parse inputValue = + match inputValue with + | ObjectValue x -> mapInput x + | NullValue -> ValueNone |> Ok + | VariableName variableName -> + match variables.TryGetValue variableName with + | true, (:? ObjectListFilter as filter) -> ValueSome filter |> Ok + | true, value -> + System.Diagnostics.Debug.Fail "We expect the root value is parsed into ObjectListFilter" + value |> InputValue.OfObject |> parse + | false, _ -> Errors.Variables.getVariableNotFoundError variableName + // TODO: Get union case + | _ -> + Error [ + { new IGQLError with + member _.Message = $"'ObjectListFilter' must be defined as object but got '{inputValue.GetType ()}'" + } + ] + parse inputValue -let private coerceObjectListFilterValue (x : obj) : ObjectListFilter option = - match x with - | :? ObjectListFilter as x -> Some x - | _ -> None -//let private coerceObjectListFilterValue (x : obj) = -// match x with -// | :? ObjectListFilter as x -> Ok x -// | _ -> Error [{ new IGQLError with member _.Message = $"Cannot coerce ObjectListFilter output. '%s{x.GetType().FullName}' is not 'ObjectListFilter'" }] - -// TODO: Move to shared and make public -let rec private jsonElementToInputValue (element : JsonElement) = - match element.ValueKind with - | JsonValueKind.Null -> NullValue - | JsonValueKind.True -> BooleanValue true - | JsonValueKind.False -> BooleanValue false - | JsonValueKind.String -> StringValue (element.GetString ()) - | JsonValueKind.Number -> FloatValue (element.GetDouble ()) - | JsonValueKind.Array -> - ListValue ( - element.EnumerateArray () - |> Seq.map jsonElementToInputValue - |> List.ofSeq - ) - | JsonValueKind.Object -> - ObjectValue ( - element.EnumerateObject () - |> Seq.map (fun p -> p.Name, jsonElementToInputValue p.Value) - |> Map.ofSeq - ) - | _ -> raise (NotSupportedException "Unsupported JSON element type") /// Defines an object list filter for use as an argument for filter list of object fields. -let ObjectListFilterType : ScalarDefinition = { +let ObjectListFilterType : InputCustomDefinition = { Name = "ObjectListFilter" Description = Some "The `Filter` scalar type represents a filter on one or more fields of an object in an object list. The filter is represented by a JSON object where the fields are the complemented by specific suffixes to represent a query." CoerceInput = - (function - | InlineConstant c -> - coerceObjectListFilterInput c - |> Result.map ValueOption.toObj - | Variable json -> - json - |> jsonElementToInputValue - |> coerceObjectListFilterInput - |> Result.map ValueOption.toObj) - CoerceOutput = coerceObjectListFilterValue + (fun input variables -> + match input with + | InlineConstant c -> + (coerceObjectListFilterInput variables c) + |> Result.map ValueOption.toObj + | Variable json -> + json + |> InputValue.OfJsonElement + |> (coerceObjectListFilterInput variables) + |> Result.map ValueOption.toObj) } diff --git a/src/FSharp.Data.GraphQL.Server/Execution.fs b/src/FSharp.Data.GraphQL.Server/Execution.fs index 17e23c13..7a3122b2 100644 --- a/src/FSharp.Data.GraphQL.Server/Execution.fs +++ b/src/FSharp.Data.GraphQL.Server/Execution.fs @@ -599,7 +599,16 @@ let internal coerceVariables (variables: VarDef list) (vars: ImmutableDictionary fun (acc : Result.Builder, IGQLError list>) struct(varDef, jsonElement) -> validation { let! value = let varTypeDef = varDef.TypeDef - coerceVariableValue false [] ValueNone (varTypeDef, varTypeDef) varDef jsonElement + let ctx = { + IsNullable = false + InputObjectPath = [] + ObjectFieldErrorDetails = ValueNone + OriginalTypeDef = varTypeDef + TypeDef = varTypeDef + VarDef = varDef + Input = jsonElement + } + coerceVariableValue ctx |> Result.mapError ( List.map (fun err -> match err with diff --git a/src/FSharp.Data.GraphQL.Server/Schema.fs b/src/FSharp.Data.GraphQL.Server/Schema.fs index 10e04b81..a23b341a 100644 --- a/src/FSharp.Data.GraphQL.Server/Schema.fs +++ b/src/FSharp.Data.GraphQL.Server/Schema.fs @@ -321,6 +321,8 @@ type Schema<'Root> (query: ObjectDef<'Root>, ?mutation: ObjectDef<'Root>, ?subsc getPossibleTypes idef |> Array.map (fun tdef -> Map.find tdef.Name namedTypes) IntrospectionType.Interface(idef.Name, idef.Description, fields, possibleTypes) + | InputCustom inCustDef -> + IntrospectionType.InputObject(inCustDef.Name, inCustDef.Description, [||]) | _ -> failwithf "Unexpected value of typedef: %O" typedef let introspectSchema (types : TypeMap) : IntrospectionSchema = @@ -334,6 +336,7 @@ type Schema<'Root> (query: ObjectDef<'Root>, ?mutation: ObjectDef<'Root>, ?subsc | Union x -> typeName, { Kind = TypeKind.UNION; Name = Some typeName; Description = x.Description; OfType = None } | Enum x -> typeName, { Kind = TypeKind.ENUM; Name = Some typeName; Description = x.Description; OfType = None } | Interface x -> typeName, { Kind = TypeKind.INTERFACE; Name = Some typeName; Description = x.Description; OfType = None } + | InputCustom x -> typeName, { Kind = TypeKind.INPUT_OBJECT; Name = Some typeName; Description = x.Description; OfType = None } | _ -> failwithf "Unexpected value of typedef: %O" typedef) |> Map.ofSeq let itypes = diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index dabc5839..1e16178d 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -103,6 +103,8 @@ let rec internal compileByType | Scalar scalardef -> variableOrElse (InlineConstant >> scalardef.CoerceInput) + | InputCustom customDef -> fun value variables -> customDef.CoerceInput (InlineConstant value) variables + | InputObject objDef -> let objtype = objDef.Type let ctor = ReflectionHelper.matchConstructor objtype (objDef.Fields |> Array.map (fun x -> x.Name)) @@ -125,8 +127,7 @@ let rec internal compileByType | Some field -> let isParameterSkippable = ReflectionHelper.isParameterSkippable param match field.TypeDef with - | Nullable _ when field.IsSkippable <> isParameterSkippable -> - skippableMismatchParameters.Add param.Name |> ignore + | Nullable _ when field.IsSkippable <> isParameterSkippable -> skippableMismatchParameters.Add param.Name |> ignore | Nullable _ when not (isParameterSkippable) && ReflectionHelper.isPrameterMandatory param @@ -140,7 +141,8 @@ let rec internal compileByType else inputDef.Type, param.ParameterType if ReflectionHelper.isAssignableWithUnwrap inputType paramType then - allParameters.Add (struct (ValueSome field, param)) |> ignore + allParameters.Add (struct (ValueSome field, param)) + |> ignore else // TODO: Consider improving by specifying type mismatches typeMismatchParameters.Add param.Name |> ignore @@ -220,25 +222,28 @@ let rec internal compileByType |> Seq.map (fun struct (field, param) -> match field with | ValueSome field -> result { - match Map.tryFind field.Name props with - | None when field.IsSkippable -> return Activator.CreateInstance param.ParameterType - | None -> return wrapOptionalNone param.ParameterType field.TypeDef.Type - | Some prop -> - let! value = - field.ExecuteInput prop variables - |> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field - if field.IsSkippable then - let innerType = param.ParameterType.GenericTypeArguments[0] - if not (ReflectionHelper.isTypeOptional innerType) && - (value = null || (innerType.IsValueType && value = Activator.CreateInstance innerType)) - then - return Activator.CreateInstance param.ParameterType - else - let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType - return normalizeOptional innerType value |> ``include`` + match Map.tryFind field.Name props with + | None when field.IsSkippable -> return Activator.CreateInstance param.ParameterType + | None -> return wrapOptionalNone param.ParameterType field.TypeDef.Type + | Some prop -> + let! value = + field.ExecuteInput prop variables + |> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field + if field.IsSkippable then + let innerType = param.ParameterType.GenericTypeArguments[0] + if + not (ReflectionHelper.isTypeOptional innerType) + && (value = null + || (innerType.IsValueType + && value = Activator.CreateInstance innerType)) + then + return Activator.CreateInstance param.ParameterType else - return normalizeOptional param.ParameterType value - } + let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType + return normalizeOptional innerType value |> ``include`` + else + return normalizeOptional param.ParameterType value + } | ValueNone -> Ok <| wrapOptionalNone param.ParameterType typeof) |> Seq.toList @@ -400,33 +405,55 @@ let rec internal compileByType Debug.Fail "Unexpected InputDef" failwithf "Unexpected value of inputDef: %O" inputDef -let rec internal coerceVariableValue - isNullable - inputObjectPath - (objectFieldErrorDetails : ObjectFieldErrorDetails voption) - (originalTypeDef, typeDef) - (varDef : VarDef) - (input : JsonElement) - : Result = +type CoerceVariableContext = { + IsNullable : bool + InputObjectPath : FieldPath + ObjectFieldErrorDetails : ObjectFieldErrorDetails voption + OriginalTypeDef : InputDef + TypeDef : InputDef + VarDef : VarDef + Input : JsonElement +} + +type CoerceVariableInputContext = { + InputObjectPath : FieldPath + OriginalObjectDef : InputDef + ObjectDef : InputObjectDef + VarDef : VarDef + Input : JsonElement +} + +let rec internal coerceVariableValue (ctx : CoerceVariableContext) : Result = + + //let { + // IsNullable = isNullable + // InputObjectPath = inputObjectPath + // ObjectFieldErrorDetails = objectFieldErrorDetails + // OriginalTypeDef = originalTypeDef + // TypeDef = typeDef + // VarDef = varDef + // Input = input + // } = + // ctx let createVariableCoercionError message = Error [ { - CoercionError.InputSource = Variable varDef + CoercionError.InputSource = Variable ctx.VarDef CoercionError.Message = message CoercionError.ErrorKind = InputCoercion - CoercionError.Path = inputObjectPath - CoercionError.FieldErrorDetails = objectFieldErrorDetails + CoercionError.Path = ctx.InputObjectPath + CoercionError.FieldErrorDetails = ctx.ObjectFieldErrorDetails } :> IGQLError ] let createNullError typeDef = let message = - match objectFieldErrorDetails with + match ctx.ObjectFieldErrorDetails with | ValueSome details -> $"Non-nullable field '%s{details.FieldDef.Value.Name}' expected value of type '%s{string typeDef}', but got 'null'." - | ValueNone -> $"Non-nullable variable '$%s{varDef.Name}' expected value of type '%s{string typeDef}', but got 'null'." + | ValueNone -> $"Non-nullable variable '$%s{ctx.VarDef.Name}' expected value of type '%s{string typeDef}', but got 'null'." createVariableCoercionError message let mapInputError varDef inputObjectPath (objectFieldErrorDetails : ObjectFieldErrorDetails voption) (err : IGQLError) : IGQLError = { @@ -437,44 +464,58 @@ let rec internal coerceVariableValue FieldErrorDetails = objectFieldErrorDetails } - match typeDef with + match ctx.TypeDef with | Scalar scalardef -> - if input.ValueKind = JsonValueKind.Null then - createNullError originalTypeDef + if ctx.Input.ValueKind = JsonValueKind.Null then + createNullError ctx.OriginalTypeDef else - match scalardef.CoerceInput (InputParameterValue.Variable input) with - | Ok null when isNullable -> Ok null + match scalardef.CoerceInput (InputParameterValue.Variable ctx.Input) with + | Ok null when ctx.IsNullable -> Ok null // TODO: Capture position in the JSON document - | Ok null -> createNullError originalTypeDef - | Ok value when not isNullable -> + | Ok null -> createNullError ctx.OriginalTypeDef + | Ok value when not ctx.IsNullable -> let ``type`` = value.GetType () if ``type``.IsValueType && ``type``.FullName.StartsWith ReflectionHelper.ValueOptionTypeName && value = Activator.CreateInstance ``type`` then - createNullError originalTypeDef + createNullError ctx.OriginalTypeDef else Ok value | result -> result - |> Result.mapError (List.map (mapInputError varDef inputObjectPath objectFieldErrorDetails)) + |> Result.mapError (List.map (mapInputError ctx.VarDef ctx.InputObjectPath ctx.ObjectFieldErrorDetails)) | Nullable (InputObject innerdef) -> - if input.ValueKind = JsonValueKind.Null then + if ctx.Input.ValueKind = JsonValueKind.Null then Ok null else - coerceVariableValue true inputObjectPath ValueNone (typeDef, innerdef :> InputDef) varDef input + let ctx' = { + ctx with + IsNullable = true + ObjectFieldErrorDetails = ValueNone + OriginalTypeDef = ctx.TypeDef + TypeDef = innerdef :> InputDef + } + coerceVariableValue ctx' | Nullable (Input innerdef) -> - if input.ValueKind = JsonValueKind.Null then + if ctx.Input.ValueKind = JsonValueKind.Null then Ok null else - coerceVariableValue true inputObjectPath ValueNone (typeDef, innerdef) varDef input + let ctx' = { + ctx with + IsNullable = true + ObjectFieldErrorDetails = ValueNone + OriginalTypeDef = ctx.TypeDef + TypeDef = innerdef + } + coerceVariableValue ctx' | List (Input innerDef) -> let cons, nil = ReflectionHelper.listOfType innerDef.Type - match input with - | _ when input.ValueKind = JsonValueKind.Null && isNullable -> Ok null - | _ when input.ValueKind = JsonValueKind.Null -> createNullError typeDef + match ctx.Input with + | _ when ctx.Input.ValueKind = JsonValueKind.Null && ctx.IsNullable -> Ok null + | _ when ctx.Input.ValueKind = JsonValueKind.Null -> createNullError ctx.TypeDef | _ -> result { let areItemsNullable = match innerDef with @@ -482,12 +523,20 @@ let rec internal coerceVariableValue | _ -> false let! items = - if input.ValueKind = JsonValueKind.Array then + if ctx.Input.ValueKind = JsonValueKind.Array then result { let! items = - input.EnumerateArray () + ctx.Input.EnumerateArray () |> Seq.mapi (fun i elem -> - coerceVariableValue areItemsNullable ((box i) :: inputObjectPath) ValueNone (originalTypeDef, innerDef) varDef elem) + let ctx' = { + ctx with + IsNullable = areItemsNullable + InputObjectPath = (box i) :: ctx.InputObjectPath + ObjectFieldErrorDetails = ValueNone + TypeDef = innerDef + Input = elem + } + coerceVariableValue ctx') |> Seq.toList |> splitSeqErrorsList if areItemsNullable then @@ -501,7 +550,14 @@ let rec internal coerceVariableValue } else result { - let! single = coerceVariableValue areItemsNullable inputObjectPath ValueNone (innerDef, innerDef) varDef input + let ctx' = { + ctx with + IsNullable = areItemsNullable + ObjectFieldErrorDetails = ValueNone + OriginalTypeDef = innerDef + TypeDef = innerDef + } + let! single = coerceVariableValue ctx' if areItemsNullable then let some, none, _ = ReflectionHelper.optionOfType innerDef.Type.GenericTypeArguments[0] @@ -512,45 +568,82 @@ let rec internal coerceVariableValue return [ single ] } - let isArray = typeDef.Type.IsArray + let isArray = ctx.TypeDef.Type.IsArray if isArray then return ReflectionHelper.arrayOfList innerDef.Type items else return List.foldBack cons items nil } - | InputObject objdef -> coerceVariableInputObject inputObjectPath (originalTypeDef, objdef) varDef input + | InputObject objdef -> + coerceVariableInputObject { + InputObjectPath = ctx.InputObjectPath + OriginalObjectDef = ctx.OriginalTypeDef + ObjectDef = objdef + VarDef = ctx.VarDef + Input = ctx.Input + } | Enum enumdef -> - match input with - | _ when input.ValueKind = JsonValueKind.Null && isNullable -> Ok null - | _ when input.ValueKind = JsonValueKind.Null -> - createVariableCoercionError $"A variable '$%s{varDef.Name}' expected value of type '%s{enumdef.Name}!', but no value was found." - | _ when input.ValueKind = JsonValueKind.String -> - let value = input.GetString () + match ctx.Input with + | _ when ctx.Input.ValueKind = JsonValueKind.Null && ctx.IsNullable -> Ok null + | _ when ctx.Input.ValueKind = JsonValueKind.Null -> + createVariableCoercionError $"A variable '$%s{ctx.VarDef.Name}' expected value of type '%s{enumdef.Name}!', but no value was found." + | _ when ctx.Input.ValueKind = JsonValueKind.String -> + let value = ctx.Input.GetString () match enumdef.Options |> Array.tryFind (fun o -> o.Name.Equals (value, StringComparison.InvariantCultureIgnoreCase)) with | Some option -> Ok option.Value | None -> createVariableCoercionError $"A value '%s{value}' is not defined in Enum '%s{enumdef.Name}'." - | _ -> createVariableCoercionError $"Enum values must be strings but got '%O{input.ValueKind}'." - | _ -> failwith $"Variable '$%s{varDef.Name}': Only Scalars, Nullables, Lists, and InputObjects are valid type definitions." + | _ -> createVariableCoercionError $"Enum values must be strings but got '%O{ctx.Input.ValueKind}'." + | InputCustom custDef -> + if ctx.Input.ValueKind = JsonValueKind.Null then + createNullError ctx.OriginalTypeDef + else + match custDef.CoerceInput (InputParameterValue.Variable ctx.Input) ImmutableDictionary.Empty with + | Ok null when ctx.IsNullable -> Ok null + // TODO: Capture position in the JSON document + | Ok null -> createNullError ctx.OriginalTypeDef + | Ok value when not ctx.IsNullable -> + let ``type`` = value.GetType () + if + ``type``.IsValueType + && ``type``.FullName.StartsWith ReflectionHelper.ValueOptionTypeName + && value = Activator.CreateInstance ``type`` + then + createNullError ctx.OriginalTypeDef + else + Ok value + | result -> + result + |> Result.mapError (List.map (mapInputError ctx.VarDef ctx.InputObjectPath ctx.ObjectFieldErrorDetails)) + | _ -> failwith $"Variable '$%s{ctx.VarDef.Name}': Only Scalars, Nullables, Lists, and InputObjects are valid type definitions." -and private coerceVariableInputObject inputObjectPath (originalObjDef, objDef) (varDef : VarDef) (input : JsonElement) = - match input.ValueKind with +and private coerceVariableInputObject (ctx : CoerceVariableInputContext) = + match ctx.Input.ValueKind with | JsonValueKind.Object -> result { let mappedResult = - objDef.Fields + ctx.ObjectDef.Fields |> Seq.vchoose (fun field -> let inline coerce value = - let inputObjectPath' = (box field.Name) :: inputObjectPath + let inputObjectPath' = (box field.Name) :: ctx.InputObjectPath let objectFieldErrorDetails = ValueSome - <| { ObjectDef = originalObjDef; FieldDef = ValueSome field } + <| { ObjectDef = ctx.OriginalObjectDef; FieldDef = ValueSome field } let fieldTypeDef = field.TypeDef let value = - coerceVariableValue false inputObjectPath' objectFieldErrorDetails (fieldTypeDef, fieldTypeDef) varDef value + let ctx = { + IsNullable = false + InputObjectPath = inputObjectPath' + ObjectFieldErrorDetails = objectFieldErrorDetails + OriginalTypeDef = fieldTypeDef + TypeDef = fieldTypeDef + VarDef = ctx.VarDef + Input = value + } + coerceVariableValue ctx KeyValuePair (field.Name, value) - match input.TryGetProperty field.Name with + match ctx.Input.TryGetProperty field.Name with | true, value -> coerce value |> ValueSome | false, _ when field.IsSkippable -> ValueNone | false, _ -> @@ -564,20 +657,20 @@ and private coerceVariableInputObject inputObjectPath (originalObjDef, objDef) ( // TODO: Improve without creating a dictionary // This also causes incorrect error messages and extensions to be generated let variables = - seq { KeyValuePair (varDef.Name, mapped :> obj) } + seq { KeyValuePair (ctx.VarDef.Name, mapped :> obj) } |> ImmutableDictionary.CreateRange - return! objDef.ExecuteInput (VariableName varDef.Name) variables + return! ctx.ObjectDef.ExecuteInput (VariableName ctx.VarDef.Name) variables } | JsonValueKind.Null -> Ok null | valueKind -> Error [ { - InputSource = Variable varDef - Message = $"A variable '$%s{varDef.Name}' expected to be '%O{JsonValueKind.Object}' but got '%O{valueKind}'." + InputSource = Variable ctx.VarDef + Message = $"A variable '$%s{ctx.VarDef.Name}' expected to be '%O{JsonValueKind.Object}' but got '%O{valueKind}'." ErrorKind = InputCoercion - Path = inputObjectPath - FieldErrorDetails = ValueSome { ObjectDef = originalObjDef; FieldDef = ValueNone } + Path = ctx.InputObjectPath + FieldErrorDetails = ValueSome { ObjectDef = ctx.OriginalObjectDef; FieldDef = ValueNone } } :> IGQLError ] diff --git a/src/FSharp.Data.GraphQL.Shared/Ast.fs b/src/FSharp.Data.GraphQL.Shared/Ast.fs index ad19e199..c79f0711 100644 --- a/src/FSharp.Data.GraphQL.Shared/Ast.fs +++ b/src/FSharp.Data.GraphQL.Shared/Ast.fs @@ -2,6 +2,8 @@ // Copyright (c) 2016 Bazinga Technologies Inc namespace FSharp.Data.GraphQL.Ast +open System +open System.Text.Json //NOTE: For references, see https://facebook.github.io/graphql/ /// 2.2 Query Document @@ -111,6 +113,59 @@ and InputValue = /// 2.10 Variables | VariableName of string + with + static member OfObject (obj : obj) = + match obj with + | null -> NullValue + | :? int64 as value -> IntValue value + | :? int32 as value -> IntValue (int64 value) + | :? int16 as value -> IntValue (int64 value) + | :? double as value -> FloatValue value + | :? single as value -> FloatValue (double value) + | :? bool as value -> BooleanValue value + | :? string as value -> StringValue value + | :? uint64 as value -> IntValue (int64 value) + | :? uint32 as value -> IntValue (int64 value) + | :? uint16 as value -> IntValue (int64 value) + | value -> + let ``type`` = value.GetType() + if ``type``.IsArray then + let array = value :?> System.Array + let list = [ for i in 0 .. array.Length - 1 -> InputValue.OfObject (array.GetValue i) ] + ListValue list + else + let genericType = ``type``.GetGenericTypeDefinition() + if typeof>.IsAssignableFrom genericType then + let dict = value :?> System.Collections.Generic.IReadOnlyDictionary + let map = + dict + |> Seq.map (fun kv -> kv.Key.ToString(), InputValue.OfObject kv.Value) + |> Map.ofSeq + ObjectValue map + else + failwith "Cannot convert object to 'InputValue'" + + static member OfJsonElement (element : JsonElement) = + match element.ValueKind with + | JsonValueKind.Null -> NullValue + | JsonValueKind.True -> BooleanValue true + | JsonValueKind.False -> BooleanValue false + | JsonValueKind.String -> StringValue (element.GetString ()) + | JsonValueKind.Number -> FloatValue (element.GetDouble ()) + | JsonValueKind.Array -> + ListValue ( + element.EnumerateArray () + |> Seq.map InputValue.OfJsonElement + |> List.ofSeq + ) + | JsonValueKind.Object -> + ObjectValue ( + element.EnumerateObject () + |> Seq.map (fun p -> p.Name, InputValue.OfJsonElement p.Value) + |> Map.ofSeq + ) + | _ -> raise (NotSupportedException "Unsupported JSON element type") + /// 2.2.8 Variables and VariableDefinition = { VariableName : string; Type : InputType; DefaultValue : InputValue option } diff --git a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs index ff2eaecf..8aa47282 100644 --- a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs @@ -62,6 +62,10 @@ module SchemaDefinitions = let getParseError destinationType value = Error [{ new IGQLError with member _.Message = $"Inline value '%s{value}' cannot be parsed into %s{destinationType}" }] + module Variables = + + let getVariableNotFoundError (variableName : string) = + Error [{ new IGQLError with member _.Message = $"A variable '$%s{variableName}' not found" }] open System.Globalization open Errors diff --git a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs index baeaceb3..05d41ed7 100644 --- a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs +++ b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs @@ -70,7 +70,7 @@ module Introspection = Args : IntrospectionInputVal[] } - /// Introspection descriptor of a GraphQL type defintion. + /// Introspection descriptor of a GraphQL type definition. and IntrospectionType = { /// Which kind category current type belongs to. Kind : TypeKind @@ -81,7 +81,7 @@ module Introspection = /// Array of field descriptors defined within current type. /// Only present for Object and Interface types. Fields : IntrospectionField[] option - /// Array of interfaces implemented by output object type defintion. + /// Array of interfaces implemented by output object type definition. Interfaces : IntrospectionTypeRef[] option /// Array of type references being possible implementation of current type. /// Only present for Union types (list of union cases) and Interface types @@ -234,7 +234,7 @@ module Introspection = } /// - /// Constructs an introspection type reference for any named type defintion + /// Constructs an introspection type reference for any named type definition /// (any type other than List or NonNull) with unique name included. /// /// Introspection type descriptor to construct reference from. @@ -304,6 +304,8 @@ module Introspection = Directives : IntrospectionDirective array } +type Variables = IReadOnlyDictionary + /// Represents a subscription as described in the schema. type Subscription = { /// The name of the subscription type in the schema. @@ -571,7 +573,7 @@ and TypeDef<'Val> = inherit TypeDef end -/// Representation of all type defintions, that can be uses as inputs. +/// Representation of all type definitions, that can be uses as inputs. /// By default only scalars, enums, lists, nullables and input objects /// are valid input types. and InputDef = @@ -579,7 +581,7 @@ and InputDef = inherit TypeDef end -/// Representation of all type defintions, that can be uses as inputs. +/// Representation of all type definitions, that can be uses as inputs. /// By default only scalars, enums, lists, nullables and input objects /// are valid input types. Constrained to represent .NET type provided /// as generic parameter. @@ -589,7 +591,7 @@ and InputDef<'Val> = inherit TypeDef<'Val> end -/// Representation of all type defintions, that can be uses as outputs. +/// Representation of all type definitions, that can be uses as outputs. /// By default only scalars, enums, lists, nullables, unions, interfaces /// and objects are valid output types. and OutputDef = @@ -597,7 +599,7 @@ and OutputDef = inherit TypeDef end -/// Representation of all type defintions, that can be uses as outputs. +/// Representation of all type definitions, that can be uses as outputs. /// By default only scalars, enums, lists, nullables, unions, interfaces /// and objects are valid input types. Constrained to represent .NET type /// provided as generic parameter. @@ -661,7 +663,7 @@ and Includer = ImmutableDictionary -> Result /// A node representing part of the current GraphQL query execution plan. /// It contains info about both document AST fragment of incoming query as well, -/// as field defintion and type info of related fields, defined in schema. +/// as field definition and type info of related fields, defined in schema. and ExecutionInfo = { /// Field identifier, which may be either field name or alias. For top level execution plan it will be None. Identifier : string @@ -864,7 +866,7 @@ and SchemaCompileContext = { Schema : ISchema; TypeMap : TypeMap; FieldExecuteMa and ExecutionPlan = { /// Unique identifier of the current execution plan. DocumentId : int - /// AST defintion of current operation. + /// AST definition of current operation. Operation : OperationDefinition /// Definition of the root type (either query or mutation) used by the /// current operation. @@ -952,7 +954,7 @@ and ResolveFieldContext = { /// Function type for the compiled field executor. and ExecuteField = ResolveFieldContext -> obj -> AsyncVal -/// Untyped representation of the GraphQL field defintion. +/// Untyped representation of the GraphQL field definition. /// Can be used only withing object and interface definitions. and FieldDef = interface @@ -974,7 +976,7 @@ and FieldDef = inherit IEquatable end -/// A paritally typed representation of the GraphQL field defintion. +/// A paritally typed representation of the GraphQL field definition. /// Contains type parameter describing .NET type used as it's container. /// Can be used only withing object and interface definitions. and FieldDef<'Val> = @@ -1301,7 +1303,7 @@ and [] internal ObjectDefinition<'Val> = { override x.ToString () = x.Name + "!" -/// A GraphQL interface type defintion. Interfaces are composite +/// A GraphQL interface type definition. Interfaces are composite /// output types, that can be implemented by GraphQL objects. and InterfaceDef = interface @@ -1323,7 +1325,7 @@ and InterfaceDef = inherit NamedDef end -/// A GraphQL interface type defintion. Interfaces are composite +/// A GraphQL interface type definition. Interfaces are composite /// output types, that can be implemented by GraphQL objects. and InterfaceDef<'Val> = interface @@ -1340,7 +1342,7 @@ and [] internal InterfaceDefinition<'Val> = { Name : string /// Optional interface description. Description : string option - /// Lazy defintion of fields to be defined by implementing + /// Lazy definition of fields to be defined by implementing /// object definition in order to satisfy current interface. /// Must be lazy in order to allow self-referencing types. FieldsFn : unit -> FieldDef<'Val>[] @@ -1691,7 +1693,7 @@ and InputObjectDefinition<'Val> = { override x.ToString () = x.Name + "!" /// Function type used for resolving input object field values. -and ExecuteInput = InputValue -> IReadOnlyDictionary -> Result +and ExecuteInput = InputValue -> Variables -> Result /// GraphQL field input definition. Can be used as fields for /// input objects or as arguments for any ordinary field definition. @@ -1757,6 +1759,56 @@ and [] InputFieldDefinition<'In> = { override x.ToString () = x.Name + ": " + x.TypeDef.ToString () +and internal InputCustomDef = + interface + /// Name of the input field / argument. + abstract Name : string + /// Optional input field / argument description. + abstract Description : string option + /// A function used to retrieve a .NET object from provided GraphQL query or JsonElement variable. + abstract CoerceInput : InputParameterValue -> Variables -> Result + inherit TypeDef + inherit NamedDef + inherit InputDef + inherit LeafDef + end + +and InputCustomDefinition<'Val> = internal { + Name : string + Description : string option + CoerceInput : InputParameterValue -> Variables -> Result<'Val, IGQLError list> +} with + interface TypeDef with + member _.Type = typeof<'Val> + + member x.MakeNullable () = + let nullable : NullableDefinition<_> = { OfType = x } + upcast nullable + + member x.MakeList () = + let list : ListOfDefinition<_, _> = { OfType = x } + upcast list + + interface InputDef + interface InputDef<'Val> + interface LeafDef + + interface InputCustomDef with + member x.Name = x.Name + member x.Description = x.Description + member x.CoerceInput input variables = x.CoerceInput input variables |> Result.map box + + interface NamedDef with + member x.Name = x.Name + + override x.Equals y = + match y with + | :? InputCustomDefinition<'Val> as f -> x.Name = f.Name + | _ -> false + + override x.GetHashCode () = x.Name.GetHashCode () + override x.ToString () = x.Name + "!" + and Tag = System.IComparable and TagsResolver = ResolveFieldContext -> Tag seq @@ -1890,7 +1942,7 @@ and [] SubscriptionObjectDefinition<'Val> = { override x.ToString () = x.Name + "!" -/// GraphQL directive defintion. +/// GraphQL directive definition. and DirectiveDef = { /// Directive's name - it's NOT '@' prefixed. Name : string @@ -2037,6 +2089,7 @@ and TypeMap () = | _ -> failwith "Expected a Named type!") |> Seq.filter (fun x -> not (map.ContainsKey (x.Name))) |> Seq.iter insert + | :? InputCustomDef as icdef -> add icdef.Name def overwrite | _ -> failwith "Unexpected type!" insert def @@ -2368,42 +2421,48 @@ module Resolve = module Patterns = - /// Active pattern to match GraphQL type defintion with Scalar. + /// Active pattern to match GraphQL type definition with Scalar. let (|Scalar|_|) (tdef : TypeDef) = match tdef with | :? ScalarDef as x -> ValueSome x | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with Object. + /// Active pattern to match GraphQL type definition with Object. let (|Object|_|) (tdef : TypeDef) = match tdef with | :? ObjectDef as x -> ValueSome x | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with Interface. + /// Active pattern to match GraphQL type definition with Interface. let (|Interface|_|) (tdef : TypeDef) = match tdef with | :? InterfaceDef as x -> ValueSome x | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with Union. + /// Active pattern to match GraphQL type definition with Union. let (|Union|_|) (tdef : TypeDef) = match tdef with | :? UnionDef as x -> ValueSome x | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with Enum. + /// Active pattern to match GraphQL type definition with Enum. let (|Enum|_|) (tdef : TypeDef) = match tdef with | :? EnumDef as x -> ValueSome x | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with input object. + /// Active pattern to match GraphQL type definition with input object. let (|InputObject|_|) (tdef : TypeDef) = match tdef with | :? InputObjectDef as x -> ValueSome x | _ -> ValueNone + /// Active pattern to match GraphQL type definition with custom object. + let internal (|InputCustom|_|) (tdef : TypeDef) = + match tdef with + | :? InputCustomDef as x -> ValueSome x + | _ -> ValueNone + /// Active patter to match GraphQL subscription object definitions let (|SubscriptionObject|_|) (tdef : TypeDef) = @@ -2411,43 +2470,43 @@ module Patterns = | :? SubscriptionObjectDef as x -> ValueSome x | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with List. + /// Active pattern to match GraphQL type definition with List. let (|List|_|) (tdef : TypeDef) = match tdef with | :? ListOfDef as x -> ValueSome x.OfType | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with nullable / optional types. + /// Active pattern to match GraphQL type definition with nullable / optional types. let (|Nullable|_|) (tdef : TypeDef) = match tdef with | :? NullableDef as x -> ValueSome x.OfType | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with non-null types. + /// Active pattern to match GraphQL type definition with non-null types. let (|NonNull|_|) (tdef : TypeDef) = match tdef with | :? NullableDef -> ValueNone | other -> ValueSome other - /// Active pattern to match GraphQL type defintion with valid input types. + /// Active pattern to match GraphQL type definition with valid input types. let (|Input|_|) (tdef : TypeDef) = match tdef with | :? InputDef as i -> ValueSome i | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with valid output types. + /// Active pattern to match GraphQL type definition with valid output types. let (|Output|_|) (tdef : TypeDef) = match tdef with | :? OutputDef as o -> ValueSome o | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with valid leaf types. + /// Active pattern to match GraphQL type definition with valid leaf types. let (|Leaf|_|) (tdef : TypeDef) = match tdef with | :? LeafDef as ldef -> ValueSome ldef | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with valid composite types. + /// Active pattern to match GraphQL type definition with valid composite types. let (|Composite|_|) (tdef : TypeDef) = match tdef with | :? ObjectDef @@ -2455,7 +2514,7 @@ module Patterns = | :? UnionDef -> ValueSome tdef | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with valid abstract types. + /// Active pattern to match GraphQL type definition with valid abstract types. let (|Abstract|_|) (tdef : TypeDef) = match tdef with | :? InterfaceDef @@ -2469,5 +2528,5 @@ module Patterns = | List inner -> named inner | _ -> ValueNone - /// Active pattern to match GraphQL type defintion with named types. + /// Active pattern to match GraphQL type definition with named types. let rec (|Named|_|) (tdef : TypeDef) = named tdef diff --git a/src/FSharp.Data.GraphQL.Shared/Validation.fs b/src/FSharp.Data.GraphQL.Shared/Validation.fs index de516a3e..1e3951a4 100644 --- a/src/FSharp.Data.GraphQL.Shared/Validation.fs +++ b/src/FSharp.Data.GraphQL.Shared/Validation.fs @@ -78,6 +78,7 @@ module Types = else ValidationError [ idef.Name + " must have at least one field defined" ] nonEmptyResult + | InputCustom _ -> Success | _ -> failwithf "Unexpected value of typedef: %O" typedef let validateTypeMap (namedTypes : TypeMap) : ValidationResult = diff --git a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs index 22128285..19b86c64 100644 --- a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs @@ -1044,6 +1044,55 @@ let ``Object list filter: Must return filter information in Metadata when suppli data |> equals (upcast expected) result.Metadata.TryFind ("filters") |> wantValueSome |> seqEquals [ expectedFilter ] +[] +let ``Object list filter: Must parse filter that references variables`` () = + let query = + parse + """query testQuery($filter: String) { + A (id : 1) { + id + value + subjects (filter : { value_starts_with : $filter }) { ...Value } + } + } + + fragment Value on Subject { + ...on A { + id + value + } + ...on B { + id + value + } + }""" + let expected = + NameValueLookup.ofList [ + "A", + upcast + NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", + upcast + [ + NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] + NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ] + ] + ] + ] + do + let filterValue = "3" |> JsonDocument.Parse |> _.RootElement + let variables = ImmutableDictionary.Empty.Add ("filter", filterValue) + let filter = (StartsWith { FieldName = "value"; Value = "3" }) + let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) + let result = executeAndVerifyFilter (query, variables, filter) + + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + result.Metadata.TryFind ("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + [] let ``Object list filter: Must return empty filter when all discriminated union types are specified`` () = let query =