Skip to content

Commit 7a9b21d

Browse files
committed
Implemented check if input object CLR property type does not match the scalar's CLR type declared in GraphQL object definition
1 parent e2ff051 commit 7a9b21d

File tree

3 files changed

+118
-20
lines changed

3 files changed

+118
-20
lines changed

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,28 @@ module internal ReflectionHelper =
140140

141141
let isPrameterMandatory = not << isParameterOptional
142142

143+
let unwrapOptions (ty : Type) =
144+
if ty.FullName.StartsWith OptionTypeName || ty.FullName.StartsWith ValueOptionTypeName then
145+
ty.GetGenericArguments().[0]
146+
else ty
147+
148+
let isAssignableWithUnwrap (from: Type) (``to``: Type) =
149+
let from =
150+
if from.FullName.StartsWith OptionTypeName || from.FullName.StartsWith ValueOptionTypeName then
151+
from.GetGenericArguments().[0]
152+
else from
153+
let ``to`` =
154+
if ``to``.FullName.StartsWith OptionTypeName || ``to``.FullName.StartsWith ValueOptionTypeName then
155+
``to``.GetGenericArguments().[0]
156+
else ``to``
157+
158+
let result = from.IsAssignableTo ``to``
159+
if result then result
160+
else
161+
if from.FullName.StartsWith OptionTypeName || from.FullName.StartsWith ValueOptionTypeName then
162+
from.GetGenericArguments().[0].IsAssignableTo ``to``
163+
else result
164+
143165
let matchConstructor (t: Type) (fields: string []) =
144166
if FSharpType.IsRecord(t, true) then FSharpValue.PreComputeRecordConstructorInfo(t, true)
145167
else

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

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ let private normalizeOptional (outputType : Type) value =
3636
| value ->
3737
let inputType = value.GetType ()
3838
if inputType.Name <> outputType.Name then
39-
let expectedOutputType = outputType.GenericTypeArguments[0]
39+
// Use only when option or voption so must not be null
40+
let expectedOutputType = outputType.GenericTypeArguments.FirstOrDefault()
4041
if
4142
outputType.FullName.StartsWith ReflectionHelper.OptionTypeName
4243
&& expectedOutputType.IsAssignableFrom inputType
@@ -50,7 +51,8 @@ let private normalizeOptional (outputType : Type) value =
5051
let valuesome, _, _ = ReflectionHelper.vOptionOfType expectedOutputType
5152
valuesome value
5253
else
53-
let realInputType = inputType.GenericTypeArguments[0]
54+
// Use only when option or voption so must not be null
55+
let realInputType = inputType.GenericTypeArguments.FirstOrDefault()
5456
if
5557
inputType.FullName.StartsWith ReflectionHelper.OptionTypeName
5658
&& outputType.IsAssignableFrom realInputType
@@ -107,10 +109,17 @@ let rec internal compileByType
107109
let objtype = objDef.Type
108110
let ctor = ReflectionHelper.matchConstructor objtype (objDef.Fields |> Array.map (fun x -> x.Name))
109111

110-
let struct (mapper, nullableMismatchParameters, missingParameters) =
112+
let struct (mapper, typeMismatchParameters, nullableMismatchParameters, missingParameters) =
111113
ctor.GetParameters ()
112114
|> Array.fold
113-
(fun struct (all : ResizeArray<_>, areNullable : HashSet<_>, missing : HashSet<_>) param ->
115+
(fun struct (
116+
all : ResizeArray<_>,
117+
mismatch : HashSet<_>,
118+
areNullable : HashSet<_>,
119+
missing : HashSet<_>
120+
)
121+
param
122+
->
114123
match
115124
objDef.Fields
116125
|> Array.tryFind (fun field -> field.Name = param.Name)
@@ -122,27 +131,41 @@ let rec internal compileByType
122131
&& field.DefaultValue.IsNone
123132
->
124133
areNullable.Add param.Name |> ignore
125-
| _ -> all.Add (struct (ValueSome field, param)) |> ignore
134+
| inputDef ->
135+
if ReflectionHelper.isAssignableWithUnwrap inputDef.Type param.ParameterType then
136+
all.Add (struct (ValueSome field, param)) |> ignore
137+
else
138+
// TODO: Consider improving by specifying type mismatches
139+
mismatch.Add param.Name |> ignore
126140
| None ->
127141
if ReflectionHelper.isParameterOptional param then
128142
all.Add <| struct (ValueNone, param) |> ignore
129143
else
130144
missing.Add param.Name |> ignore
131-
struct (all, areNullable, missing))
132-
struct (ResizeArray (), HashSet (), HashSet ())
133-
134-
if missingParameters.Any () then
135-
raise
136-
<| InvalidInputTypeException (
137-
$"Input object '%s{objDef.Name}' refers to type '%O{objtype}', but mandatory constructor parameters '%A{missingParameters}' don't match any of the defined input fields",
138-
missingParameters.ToImmutableHashSet ()
139-
)
140-
if nullableMismatchParameters.Any () then
141-
raise
142-
<| InvalidInputTypeException (
143-
$"Input object %s{objDef.Name} refers to type '%O{objtype}', but optional fields '%A{missingParameters}' are not optional parameters of the constructor",
144-
nullableMismatchParameters.ToImmutableHashSet ()
145-
)
145+
struct (all, mismatch, areNullable, missing))
146+
struct (ResizeArray (), HashSet (), HashSet (), HashSet ())
147+
148+
let exceptions : exn list = [
149+
if missingParameters.Any () then
150+
InvalidInputTypeException (
151+
$"Input object '%s{objDef.Name}' refers to type '%O{objtype}', but mandatory constructor parameters '%A{missingParameters}' don't match any of the defined input fields",
152+
missingParameters.ToImmutableHashSet ()
153+
)
154+
if nullableMismatchParameters.Any () then
155+
InvalidInputTypeException (
156+
$"Input object %s{objDef.Name} refers to type '%O{objtype}', but optional fields '%A{missingParameters}' are not optional parameters of the constructor",
157+
nullableMismatchParameters.ToImmutableHashSet ()
158+
)
159+
if typeMismatchParameters.Any () then
160+
InvalidInputTypeException (
161+
$"Input object %s{objDef.Name} refers to type '%O{objtype}', but fields '%A{typeMismatchParameters}' have different types than constructor parameters",
162+
typeMismatchParameters.ToImmutableHashSet ()
163+
)
164+
]
165+
match exceptions with
166+
| [] -> ()
167+
| [ ex ] -> raise ex
168+
| _ -> raise (AggregateException ($"Invalid input object '%O{objtype}'", exceptions))
146169

147170
let attachErrorExtensionsIfScalar inputSource path objDef (fieldDef : InputFieldDef) result =
148171

tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputScalarAndAutoFieldScalarTests.fs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module FSharp.Data.GraphQL.Tests.InputScalarAndAutoFieldScalarTests
66

77
open Xunit
88
open System
9+
open System.Text.Json.Serialization
910

1011
open FSharp.Data.GraphQL
1112
open FSharp.Data.GraphQL.Types
@@ -210,3 +211,55 @@ let ``Execute handles nullable auto-fields in input and output object fields coe
210211
empty errors
211212
data |> equals (upcast expected)
212213

214+
215+
open FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests
216+
217+
[<RequireQualifiedAccess>]
218+
module ConsoleLoginProviders =
219+
220+
let [<Literal>] Microsoft365 = "microsoft365"
221+
let [<Literal>] GoogleWorkspace = "google_workspace"
222+
223+
type ConsoleLoginProvider =
224+
| [<JsonName(ConsoleLoginProviders.Microsoft365)>] Microsoft365
225+
| [<JsonName(ConsoleLoginProviders.GoogleWorkspace)>] GoogleWorkspace
226+
and ApplicationTenantId = ValidString<InputRecord>
227+
and WrongInput = {
228+
Id : ApplicationTenantId
229+
/// Legal entity name
230+
Name : string
231+
LoginProvider : ConsoleLoginProvider
232+
AllowedDomains : string list
233+
/// Tenants visible to a management company
234+
AuthorizedTenants : ApplicationTenantId list
235+
}
236+
237+
// Checks that IndexOutOfRangeException no longer happens in normalizeOptional
238+
[<Fact>]
239+
let ``Schema cannot be created for unmatched input field types on record`` () =
240+
241+
let ``InputRecord without proper scalars Type`` =
242+
Define.InputObject<InputRecord> (
243+
"InputRecordWithoutProperScalars",
244+
[ Define.Input ("id", StringType)
245+
Define.Input ("name", StringType)
246+
Define.Input ("loginProvider", StringType)
247+
Define.Input ("allowedDomains", ListOf StringType)
248+
Define.Input ("authorizedTenants", ListOf GuidType) ]
249+
)
250+
251+
Assert.Throws<InvalidInputTypeException> (fun () ->
252+
Schema (
253+
query =
254+
Define.Object (
255+
"Query",
256+
fun () ->
257+
[ Define.Field (
258+
"wrongRecord",
259+
StringType,
260+
[ Define.Input ("record", ``InputRecord without proper scalars Type``) ],
261+
stringifyInput
262+
) ]
263+
)
264+
) |> Executor :> obj
265+
)

0 commit comments

Comments
 (0)