Skip to content

Commit 10caa85

Browse files
committed
Allow accepting untyped inputs
1 parent 85a9d8c commit 10caa85

1 file changed

Lines changed: 116 additions & 0 deletions

File tree

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

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ let inline tryConvertAst schema ast =
9393

9494
convert true schema ast
9595

96+
let compileByTypeCache = Collections.Concurrent.ConcurrentDictionary<InputDef, ExecuteInput>()
97+
9698
let rec internal compileByType
9799
(inputObjectPath : FieldPath)
98100
(inputSource : InputSource)
@@ -103,6 +105,120 @@ let rec internal compileByType
103105

104106
| Scalar scalarDef -> variableOrElse (InlineConstant >> scalarDef.CoerceInput)
105107
| InputCustom customDef -> fun inputContext value variables -> customDef.CoerceInput inputContext (InlineConstant value) variables
108+
109+
//| Nullable (InputObject objDef)
110+
//| InputObject objDef when typeof<IDictionary<string,obj>>.IsAssignableFrom(objDef.Type) ->
111+
| InputObject objDef when typeof<Dynamic.ExpandoObject>.IsAssignableFrom(objDef.Type) ->
112+
113+
let executeInputFn = compileByTypeCache.GetOrAdd(objDef, Func<_,_>(fun _ ->
114+
let nestedFields =
115+
seq {
116+
for fld in objDef.Fields do
117+
match fld.TypeDef with
118+
| InputObject innerObjDef
119+
| Nullable (InputObject innerObjDef)
120+
| List (InputObject innerObjDef) ->
121+
// If the field contains a nested input object with a proper InputObject type definition
122+
// then we'll use the built in `executeInput` functionality to populate it.
123+
// **Caveat**: some of these objects may be recursively defined, so we need to delay the invocation of compileByType,
124+
// otherwise we get stack overflow at GraphQL schema compilation time.
125+
let delayedExecuteInputStub getInputContext value variables =
126+
let innerExecuteInput =
127+
compileByTypeCache.GetOrAdd(fld.TypeDef, Func<_,_>(fun _ ->
128+
compileByType (box fld.Name :: inputObjectPath) inputSource (fld.TypeDef, fld.TypeDef) getInputContext
129+
))
130+
innerObjDef.ExecuteInput <- innerExecuteInput // replace ExecuteInput with true function on first call
131+
innerExecuteInput getInputContext value variables // call it
132+
innerObjDef.ExecuteInput <- delayedExecuteInputStub
133+
yield KeyValuePair(fld.Name, fld)
134+
| _ -> ()
135+
}
136+
|> Collections.Frozen.FrozenDictionary.ToFrozenDictionary
137+
138+
// Helper for populating fields from untyped input values
139+
let rec getRawInputValue (variables:Variables) (value:InputValue) =
140+
match value with
141+
| NullValue -> ValueNone
142+
| StringValue s -> ValueSome (box s)
143+
| IntValue i -> ValueSome (box i)
144+
| FloatValue f -> ValueSome (box f)
145+
| BooleanValue b -> ValueSome (box b)
146+
| EnumValue e -> ValueSome (box e)
147+
| ListValue lst -> ValueSome (box (lst |> Seq.map (fun el ->
148+
match getRawInputValue variables el with
149+
| ValueNone -> null
150+
| ValueSome v -> v
151+
)))
152+
| ObjectValue o ->
153+
let dict = Dictionary<string, obj>()
154+
for KeyValue (key, prop) in o do
155+
match getRawInputValue variables prop with
156+
| ValueNone -> ()
157+
| ValueSome v -> dict[key] <- v
158+
ValueSome (box dict)
159+
| VariableName varName -> ValueSome (variables[varName])
160+
161+
// Populate dictionary from parsed ObjectValue (Map<string, InputValue>)
162+
let populateFromTypedInputObj (props: Map<string, InputValue>) (variables:Variables) = result {
163+
let o = Activator.CreateInstance(objDef.Type)
164+
let dict = o :?> IDictionary<string, obj>
165+
for KeyValue (key, prop) in props do
166+
match nestedFields.TryGetValue key with
167+
| true, fieldDef ->
168+
let! coerced = fieldDef.ExecuteInput getInputContext prop variables
169+
dict[key] <- normalizeOptional fieldDef.TypeDef.Type coerced
170+
| false, _ ->
171+
match prop |> getRawInputValue variables with
172+
| ValueNone -> ()
173+
| ValueSome v -> dict[key] <- v
174+
return o
175+
}
176+
177+
// Populate dictionary from variable value (untyped object)
178+
let populateFromUntypedInputObj (props: IReadOnlyDictionary<string, obj>) (variables:Variables) = result {
179+
let o = Activator.CreateInstance(objDef.Type)
180+
let dict = o :?> IDictionary<string, obj>
181+
for KeyValue (key, value) in props do
182+
match nestedFields.TryGetValue key with
183+
| true, fieldDef ->
184+
let inputValue = InputValue.OfObject value
185+
let! coerced =
186+
fieldDef.ExecuteInput getInputContext inputValue variables
187+
dict[key] <- normalizeOptional fieldDef.TypeDef.Type coerced
188+
| false, _ -> dict[key] <- value
189+
return o
190+
}
191+
192+
// return function: executeInput
193+
fun ctxGetter value variables ->
194+
match value with
195+
| ObjectValue props -> populateFromTypedInputObj props variables
196+
| VariableName variableName ->
197+
match variables.TryGetValue variableName with
198+
| true, found ->
199+
match found with
200+
| :? IReadOnlyDictionary<string, obj> as objectFields ->
201+
populateFromUntypedInputObj objectFields variables
202+
| null -> Ok null
203+
| _ ->
204+
Debugger.Break ()
205+
Error [
206+
{ new IGQLError with
207+
member _.Message = $"A variable '${variableName}' is not an object"
208+
}
209+
]
210+
| false, _ -> Ok null
211+
| NullValue -> populateFromTypedInputObj Map.empty variables
212+
| IntValue _ | FloatValue _ | BooleanValue _ | StringValue _ | EnumValue _ | ListValue _ ->
213+
Error [
214+
{ new IGQLError with
215+
member _.Message = $"Input object expected, but got scalar value"
216+
}
217+
]
218+
))
219+
objDef.ExecuteInput <- executeInputFn
220+
executeInputFn
221+
106222
| InputObject objDef ->
107223
let objType = objDef.Type
108224
let ctor = ReflectionHelper.matchConstructor objType (objDef.Fields |> Array.map _.Name)

0 commit comments

Comments
 (0)