Skip to content

Commit 68218f9

Browse files
author
Viktor Tochonov
committed
Created ObjectListFilter parsing for filter value through variable
1 parent a3738b3 commit 68218f9

File tree

7 files changed

+190
-108
lines changed

7 files changed

+190
-108
lines changed

src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs

Lines changed: 37 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
module FSharp.Data.GraphQL.Server.Middleware.SchemaDefinitions
44

55
open System
6-
open System.Collections.Generic
7-
open System.Collections.Immutable
8-
open System.Text.Json
96
open FSharp.Data.GraphQL
107
open FSharp.Data.GraphQL.Types
118
open FSharp.Data.GraphQL.Ast
@@ -22,7 +19,7 @@ type private ComparisonOperator =
2219
| LessThanOrEqual of string
2320
| In of string
2421

25-
let rec private coerceObjectListFilterInput x : Result<ObjectListFilter voption, IGQLError list> =
22+
let rec private coerceObjectListFilterInput (variables : Variables) inputValue : Result<ObjectListFilter voption, IGQLError list> =
2623

2724
let parseFieldCondition (s : string) =
2825
let s = s.ToLowerInvariant ()
@@ -81,17 +78,17 @@ let rec private coerceObjectListFilterInput x : Result<ObjectListFilter voption,
8178
| ValueSome acc -> build (ValueSome (Or (acc, x))) xs
8279
build ValueNone x
8380

84-
let rec mapFilter (name : string, value : InputValue) =
81+
let rec mapFilter (condition : ComparisonOperator) (value : InputValue) =
8582
let mapFilters fields =
8683
let coerceResults =
8784
fields
88-
|> Seq.map coerceObjectListFilterInput
85+
|> Seq.map (coerceObjectListFilterInput variables)
8986
|> Seq.toList
9087
|> splitSeqErrorsList
9188
match coerceResults with
9289
| Error errs -> Error errs
9390
| Ok coerced -> coerced |> Seq.vchoose id |> Seq.toList |> Ok
94-
match parseFieldCondition name, value with
91+
match condition, value with
9592
| Equals "and", ListValue fields -> fields |> mapFilters |> Result.map buildAnd
9693
| Equals "or", ListValue fields -> fields |> mapFilters |> Result.map buildOr
9794
| Equals "not", ObjectValue value ->
@@ -126,60 +123,42 @@ let rec private coerceObjectListFilterInput x : Result<ObjectListFilter voption,
126123
|> splitSeqErrors
127124
return ValueSome (ObjectListFilter.In { FieldName = fname; Value = parsedValues |> Array.toList })
128125
}
126+
| condition, VariableName variableName ->
127+
match variables.TryGetValue variableName with
128+
| true, value -> mapFilter condition (value |> InputValue.OfObject)
129+
| false, _ -> Errors.Variables.getVariableNotFoundError variableName
129130
| _ -> Ok ValueNone
130131

131132
and mapInput value =
132133
let filterResults =
133134
value
134-
|> Map.toSeq
135-
|> Seq.map mapFilter
135+
|> Seq.map (fun kvp -> mapFilter (parseFieldCondition kvp.Key) kvp.Value)
136136
|> Seq.toList
137137
|> splitSeqErrorsList
138138
match filterResults with
139139
| Error errs -> Error errs
140140
| Ok filters -> filters |> Seq.vchoose id |> List.ofSeq |> buildAnd |> Ok
141141

142-
match x with
143-
| ObjectValue x -> mapInput x
144-
| NullValue -> ValueNone |> Ok
145-
// TODO: Get union case
146-
| _ ->
147-
Error [
148-
{ new IGQLError with
149-
member _.Message = $"'ObjectListFilter' must be defined as object but got '{x.GetType ()}'"
150-
}
151-
]
142+
let rec parse inputValue =
143+
match inputValue with
144+
| ObjectValue x -> mapInput x
145+
| NullValue -> ValueNone |> Ok
146+
| VariableName variableName ->
147+
match variables.TryGetValue variableName with
148+
| true, (:? ObjectListFilter as filter) -> ValueSome filter |> Ok
149+
| true, value ->
150+
System.Diagnostics.Debug.Fail "We expect the root value is parsed into ObjectListFilter"
151+
value |> InputValue.OfObject |> parse
152+
| false, _ -> Errors.Variables.getVariableNotFoundError variableName
153+
// TODO: Get union case
154+
| _ ->
155+
Error [
156+
{ new IGQLError with
157+
member _.Message = $"'ObjectListFilter' must be defined as object but got '{inputValue.GetType ()}'"
158+
}
159+
]
160+
parse inputValue
152161

153-
let private coerceObjectListFilterValue (x : obj) : ObjectListFilter option =
154-
match x with
155-
| :? ObjectListFilter as x -> Some x
156-
| _ -> None
157-
//let private coerceObjectListFilterValue (x : obj) =
158-
// match x with
159-
// | :? ObjectListFilter as x -> Ok x
160-
// | _ -> Error [{ new IGQLError with member _.Message = $"Cannot coerce ObjectListFilter output. '%s{x.GetType().FullName}' is not 'ObjectListFilter'" }]
161-
162-
// TODO: Move to shared and make public
163-
let rec private jsonElementToInputValue (element : JsonElement) =
164-
match element.ValueKind with
165-
| JsonValueKind.Null -> NullValue
166-
| JsonValueKind.True -> BooleanValue true
167-
| JsonValueKind.False -> BooleanValue false
168-
| JsonValueKind.String -> StringValue (element.GetString ())
169-
| JsonValueKind.Number -> FloatValue (element.GetDouble ())
170-
| JsonValueKind.Array ->
171-
ListValue (
172-
element.EnumerateArray ()
173-
|> Seq.map jsonElementToInputValue
174-
|> List.ofSeq
175-
)
176-
| JsonValueKind.Object ->
177-
ObjectValue (
178-
element.EnumerateObject ()
179-
|> Seq.map (fun p -> p.Name, jsonElementToInputValue p.Value)
180-
|> Map.ofSeq
181-
)
182-
| _ -> raise (NotSupportedException "Unsupported JSON element type")
183162

184163
/// Defines an object list filter for use as an argument for filter list of object fields.
185164
let ObjectListFilterType : InputCustomDefinition<ObjectListFilter> = {
@@ -189,13 +168,13 @@ let ObjectListFilterType : InputCustomDefinition<ObjectListFilter> = {
189168
"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."
190169
CoerceInput =
191170
(fun input variables ->
192-
match input with
193-
| InlineConstant c ->
194-
coerceObjectListFilterInput c
195-
|> Result.map ValueOption.toObj
196-
| Variable json ->
197-
json
198-
|> jsonElementToInputValue
199-
|> coerceObjectListFilterInput
200-
|> Result.map ValueOption.toObj)
171+
match input with
172+
| InlineConstant c ->
173+
(coerceObjectListFilterInput variables c)
174+
|> Result.map ValueOption.toObj
175+
| Variable json ->
176+
json
177+
|> InputValue.OfJsonElement
178+
|> (coerceObjectListFilterInput variables)
179+
|> Result.map ValueOption.toObj)
201180
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,8 @@ type Schema<'Root> (query: ObjectDef<'Root>, ?mutation: ObjectDef<'Root>, ?subsc
321321
getPossibleTypes idef
322322
|> Array.map (fun tdef -> Map.find tdef.Name namedTypes)
323323
IntrospectionType.Interface(idef.Name, idef.Description, fields, possibleTypes)
324+
| InputCustom inCustDef ->
325+
IntrospectionType.InputObject(inCustDef.Name, inCustDef.Description, [||])
324326
| _ -> failwithf "Unexpected value of typedef: %O" typedef
325327

326328
let introspectSchema (types : TypeMap) : IntrospectionSchema =
@@ -334,6 +336,7 @@ type Schema<'Root> (query: ObjectDef<'Root>, ?mutation: ObjectDef<'Root>, ?subsc
334336
| Union x -> typeName, { Kind = TypeKind.UNION; Name = Some typeName; Description = x.Description; OfType = None }
335337
| Enum x -> typeName, { Kind = TypeKind.ENUM; Name = Some typeName; Description = x.Description; OfType = None }
336338
| Interface x -> typeName, { Kind = TypeKind.INTERFACE; Name = Some typeName; Description = x.Description; OfType = None }
339+
| InputCustom x -> typeName, { Kind = TypeKind.INPUT_OBJECT; Name = Some typeName; Description = x.Description; OfType = None }
337340
| _ -> failwithf "Unexpected value of typedef: %O" typedef)
338341
|> Map.ofSeq
339342
let itypes =

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

Lines changed: 85 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ let rec internal compileByType
103103

104104
| Scalar scalardef -> variableOrElse (InlineConstant >> scalardef.CoerceInput)
105105

106-
| InputCustom customDef ->
107-
fun value variables -> customDef.CoerceInput (InlineConstant value) variables
106+
| InputCustom customDef -> fun value variables -> customDef.CoerceInput (InlineConstant value) variables
108107

109108
| InputObject objDef ->
110109
let objtype = objDef.Type
@@ -128,8 +127,7 @@ let rec internal compileByType
128127
| Some field ->
129128
let isParameterSkippable = ReflectionHelper.isParameterSkippable param
130129
match field.TypeDef with
131-
| Nullable _ when field.IsSkippable <> isParameterSkippable ->
132-
skippableMismatchParameters.Add param.Name |> ignore
130+
| Nullable _ when field.IsSkippable <> isParameterSkippable -> skippableMismatchParameters.Add param.Name |> ignore
133131
| Nullable _ when
134132
not (isParameterSkippable)
135133
&& ReflectionHelper.isPrameterMandatory param
@@ -143,7 +141,8 @@ let rec internal compileByType
143141
else
144142
inputDef.Type, param.ParameterType
145143
if ReflectionHelper.isAssignableWithUnwrap inputType paramType then
146-
allParameters.Add (struct (ValueSome field, param)) |> ignore
144+
allParameters.Add (struct (ValueSome field, param))
145+
|> ignore
147146
else
148147
// TODO: Consider improving by specifying type mismatches
149148
typeMismatchParameters.Add param.Name |> ignore
@@ -223,25 +222,28 @@ let rec internal compileByType
223222
|> Seq.map (fun struct (field, param) ->
224223
match field with
225224
| ValueSome field -> result {
226-
match Map.tryFind field.Name props with
227-
| None when field.IsSkippable -> return Activator.CreateInstance param.ParameterType
228-
| None -> return wrapOptionalNone param.ParameterType field.TypeDef.Type
229-
| Some prop ->
230-
let! value =
231-
field.ExecuteInput prop variables
232-
|> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field
233-
if field.IsSkippable then
234-
let innerType = param.ParameterType.GenericTypeArguments[0]
235-
if not (ReflectionHelper.isTypeOptional innerType) &&
236-
(value = null || (innerType.IsValueType && value = Activator.CreateInstance innerType))
237-
then
238-
return Activator.CreateInstance param.ParameterType
239-
else
240-
let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType
241-
return normalizeOptional innerType value |> ``include``
225+
match Map.tryFind field.Name props with
226+
| None when field.IsSkippable -> return Activator.CreateInstance param.ParameterType
227+
| None -> return wrapOptionalNone param.ParameterType field.TypeDef.Type
228+
| Some prop ->
229+
let! value =
230+
field.ExecuteInput prop variables
231+
|> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field
232+
if field.IsSkippable then
233+
let innerType = param.ParameterType.GenericTypeArguments[0]
234+
if
235+
not (ReflectionHelper.isTypeOptional innerType)
236+
&& (value = null
237+
|| (innerType.IsValueType
238+
&& value = Activator.CreateInstance innerType))
239+
then
240+
return Activator.CreateInstance param.ParameterType
242241
else
243-
return normalizeOptional param.ParameterType value
244-
}
242+
let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType
243+
return normalizeOptional innerType value |> ``include``
244+
else
245+
return normalizeOptional param.ParameterType value
246+
}
245247
| ValueNone -> Ok <| wrapOptionalNone param.ParameterType typeof<obj>)
246248
|> Seq.toList
247249

@@ -413,18 +415,26 @@ type CoerceVariableContext = {
413415
Input : JsonElement
414416
}
415417

416-
let rec internal coerceVariableValue (ctx : CoerceVariableContext)
417-
: Result<obj, IGQLError list> =
418+
type CoerceVariableInputContext = {
419+
InputObjectPath : FieldPath
420+
OriginalObjectDef : InputDef
421+
ObjectDef : InputObjectDef
422+
VarDef : VarDef
423+
Input : JsonElement
424+
}
425+
426+
let rec internal coerceVariableValue (ctx : CoerceVariableContext) : Result<obj, IGQLError list> =
418427

419428
let {
420-
IsNullable = isNullable
421-
InputObjectPath = inputObjectPath
422-
ObjectFieldErrorDetails = objectFieldErrorDetails
423-
OriginalTypeDef = originalTypeDef
424-
TypeDef = typeDef
425-
VarDef = varDef
426-
Input = input
427-
} = ctx
429+
IsNullable = isNullable
430+
InputObjectPath = inputObjectPath
431+
ObjectFieldErrorDetails = objectFieldErrorDetails
432+
OriginalTypeDef = originalTypeDef
433+
TypeDef = typeDef
434+
VarDef = varDef
435+
Input = input
436+
} =
437+
ctx
428438

429439
let createVariableCoercionError message =
430440
Error [
@@ -564,7 +574,14 @@ let rec internal coerceVariableValue (ctx : CoerceVariableContext)
564574
else
565575
return List.foldBack cons items nil
566576
}
567-
| InputObject objdef -> coerceVariableInputObject inputObjectPath (originalTypeDef, objdef) varDef input
577+
| InputObject objdef ->
578+
coerceVariableInputObject {
579+
InputObjectPath = inputObjectPath
580+
OriginalObjectDef = originalTypeDef
581+
ObjectDef = objdef
582+
VarDef = varDef
583+
Input = input
584+
}
568585
| Enum enumdef ->
569586
match input with
570587
| _ when input.ValueKind = JsonValueKind.Null && isNullable -> Ok null
@@ -579,19 +596,40 @@ let rec internal coerceVariableValue (ctx : CoerceVariableContext)
579596
| Some option -> Ok option.Value
580597
| None -> createVariableCoercionError $"A value '%s{value}' is not defined in Enum '%s{enumdef.Name}'."
581598
| _ -> createVariableCoercionError $"Enum values must be strings but got '%O{input.ValueKind}'."
599+
| InputCustom custDef ->
600+
if input.ValueKind = JsonValueKind.Null then
601+
createNullError originalTypeDef
602+
else
603+
match custDef.CoerceInput (InputParameterValue.Variable input) ImmutableDictionary.Empty with
604+
| Ok null when isNullable -> Ok null
605+
// TODO: Capture position in the JSON document
606+
| Ok null -> createNullError originalTypeDef
607+
| Ok value when not isNullable ->
608+
let ``type`` = value.GetType ()
609+
if
610+
``type``.IsValueType
611+
&& ``type``.FullName.StartsWith ReflectionHelper.ValueOptionTypeName
612+
&& value = Activator.CreateInstance ``type``
613+
then
614+
createNullError originalTypeDef
615+
else
616+
Ok value
617+
| result ->
618+
result
619+
|> Result.mapError (List.map (mapInputError varDef inputObjectPath objectFieldErrorDetails))
582620
| _ -> failwith $"Variable '$%s{varDef.Name}': Only Scalars, Nullables, Lists, and InputObjects are valid type definitions."
583621

584-
and private coerceVariableInputObject inputObjectPath (originalObjDef, objDef) (varDef : VarDef) (input : JsonElement) =
585-
match input.ValueKind with
622+
and private coerceVariableInputObject (ctx : CoerceVariableInputContext) =
623+
match ctx.Input.ValueKind with
586624
| JsonValueKind.Object -> result {
587625
let mappedResult =
588-
objDef.Fields
626+
ctx.ObjectDef.Fields
589627
|> Seq.vchoose (fun field ->
590628
let inline coerce value =
591-
let inputObjectPath' = (box field.Name) :: inputObjectPath
629+
let inputObjectPath' = (box field.Name) :: ctx.InputObjectPath
592630
let objectFieldErrorDetails =
593631
ValueSome
594-
<| { ObjectDef = originalObjDef; FieldDef = ValueSome field }
632+
<| { ObjectDef = ctx.OriginalObjectDef; FieldDef = ValueSome field }
595633
let fieldTypeDef = field.TypeDef
596634
let value =
597635
let ctx = {
@@ -600,12 +638,12 @@ and private coerceVariableInputObject inputObjectPath (originalObjDef, objDef) (
600638
ObjectFieldErrorDetails = objectFieldErrorDetails
601639
OriginalTypeDef = fieldTypeDef
602640
TypeDef = fieldTypeDef
603-
VarDef = varDef
641+
VarDef = ctx.VarDef
604642
Input = value
605643
}
606644
coerceVariableValue ctx
607645
KeyValuePair (field.Name, value)
608-
match input.TryGetProperty field.Name with
646+
match ctx.Input.TryGetProperty field.Name with
609647
| true, value -> coerce value |> ValueSome
610648
| false, _ when field.IsSkippable -> ValueNone
611649
| false, _ ->
@@ -619,20 +657,20 @@ and private coerceVariableInputObject inputObjectPath (originalObjDef, objDef) (
619657
// TODO: Improve without creating a dictionary
620658
// This also causes incorrect error messages and extensions to be generated
621659
let variables =
622-
seq { KeyValuePair (varDef.Name, mapped :> obj) }
660+
seq { KeyValuePair (ctx.VarDef.Name, mapped :> obj) }
623661
|> ImmutableDictionary.CreateRange
624662

625-
return! objDef.ExecuteInput (VariableName varDef.Name) variables
663+
return! ctx.ObjectDef.ExecuteInput (VariableName ctx.VarDef.Name) variables
626664
}
627665
| JsonValueKind.Null -> Ok null
628666
| valueKind ->
629667
Error [
630668
{
631-
InputSource = Variable varDef
632-
Message = $"A variable '$%s{varDef.Name}' expected to be '%O{JsonValueKind.Object}' but got '%O{valueKind}'."
669+
InputSource = Variable ctx.VarDef
670+
Message = $"A variable '$%s{ctx.VarDef.Name}' expected to be '%O{JsonValueKind.Object}' but got '%O{valueKind}'."
633671
ErrorKind = InputCoercion
634-
Path = inputObjectPath
635-
FieldErrorDetails = ValueSome { ObjectDef = originalObjDef; FieldDef = ValueNone }
672+
Path = ctx.InputObjectPath
673+
FieldErrorDetails = ValueSome { ObjectDef = ctx.OriginalObjectDef; FieldDef = ValueNone }
636674
}
637675
:> IGQLError
638676
]

0 commit comments

Comments
 (0)