Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented the ability to recognize if an input field is null or not present at all #508

Merged
merged 2 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion samples/star-wars-api/Schema.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace FSharp.Data.GraphQL.Samples.StarWarsApi

open System.Text.Json.Serialization
open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Types
open FSharp.Data.GraphQL.Server.Relay
Expand Down Expand Up @@ -27,9 +28,15 @@ type Droid =
AppearsIn : Episode list
PrimaryFunction : string option }

type PatchPlanet =
{ Name : string option Skippable
SatelitesCount : int Skippable
IsMoon : bool option Skippable }

type Planet =
{ Id : string
Name : string option
mutable Name : string option
mutable SatelitesCount : int
mutable IsMoon : bool option }

member x.SetMoon b =
Expand Down Expand Up @@ -94,12 +101,15 @@ module Schema =
let planets =
[ { Id = "1"
Name = Some "Tatooine"
SatelitesCount = 2
IsMoon = Some false}
{ Id = "2"
Name = Some "Endor"
SatelitesCount = 1
IsMoon = Some true}
{ Id = "3"
Name = Some "Death Star"
SatelitesCount = 0
IsMoon = Some false} ]

let getHuman id = humans |> List.tryFind (fun h -> h.Id = id)
Expand Down Expand Up @@ -228,10 +238,22 @@ module Schema =
fields = [
Define.Field ("id", StringType, "The id of the planet", (fun _ p -> p.Id))
Define.Field ("name", Nullable StringType, "The name of the planet.", (fun _ p -> p.Name))
Define.Field ("satelitesCount", IntType, "The number of satelites of the planet.", (fun _ p -> p.SatelitesCount))
Define.Field ("isMoon", Nullable BooleanType, "Is that a moon?", (fun _ p -> p.IsMoon))
]
)

and PatchPlanetType =
Define.InputObject<PatchPlanet> (
name = "InputPatchPlanet",
description = "A planet in the Star Wars universe.",
fields = [
Define.SkippableInput ("name", Nullable StringType)
Define.SkippableInput ("satelitesCount", IntType)
Define.SkippableInput ("isMoon", Nullable BooleanType)
]
)

and RootType =
Define.Object<Root> (
name = "Root",
Expand Down Expand Up @@ -299,6 +321,21 @@ module Schema =
//).WithAuthorizationPolicies(Policies.CanSetMoon)
// For build verification purposes
).WithAuthorizationPolicies(Policies.Dummy)
Define.Field(
"patchPlanet",
PlanetType,
[ Define.Input ("id", StringType); Define.Input ("planet", PatchPlanetType) ],
resolve = (fun ctx _ ->
match getPlanet (ctx.Arg ("id")) with
| None -> raise (GQLMessageException "Planet not found")
| Some planet ->
let patch = ctx.Arg<PatchPlanet> "planet"
patch.Name |> Skippable.iter (fun n -> planet.Name <- n)
patch.SatelitesCount |> Skippable.iter (fun s -> planet.SatelitesCount <- s)
patch.IsMoon |> Skippable.iter (fun m -> planet.IsMoon <- m)
planet
)
)
]
)

Expand Down
1 change: 1 addition & 0 deletions src/FSharp.Data.GraphQL.Server/Planning.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ let TypeMetaFieldDef =
args = [
{ Name = "name"
Description = None
IsSkippable = false
TypeDef = StringType
DefaultValue = None
ExecuteInput = variableOrElse(InlineConstant >> coerceStringInput >> Result.map box) }
Expand Down
19 changes: 14 additions & 5 deletions src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,24 @@ module internal ReflectionHelper =

let [<Literal>] OptionTypeName = "Microsoft.FSharp.Core.FSharpOption`1"
let [<Literal>] ValueOptionTypeName = "Microsoft.FSharp.Core.FSharpValueOption`1"
let [<Literal>] SkippableTypeName = "System.Text.Json.Serialization.Skippable`1"
let [<Literal>] ListTypeName = "Microsoft.FSharp.Collections.FSharpList`1"
let [<Literal>] ArrayTypeName = "System.Array`1"
let [<Literal>] IEnumerableTypeName = "System.Collections.IEnumerable"
let [<Literal>] IEnumerableGenericTypeName = "System.Collections.Generic.IEnumerable`1"

let rec isTypeOptional (t: Type) =
t.FullName.StartsWith OptionTypeName
|| t.FullName.StartsWith ValueOptionTypeName
|| (t.FullName.StartsWith SkippableTypeName && isTypeOptional (t.GetGenericArguments().[0]))

let isParameterOptional (p: ParameterInfo) =
p.IsOptional
|| p.ParameterType.FullName.StartsWith OptionTypeName
|| p.ParameterType.FullName.StartsWith ValueOptionTypeName
p.IsOptional || isTypeOptional p.ParameterType

let isPrameterMandatory = not << isParameterOptional

let isParameterSkippable (p: ParameterInfo) = p.ParameterType.FullName.StartsWith SkippableTypeName

let unwrapOptions (ty : Type) =
if ty.FullName.StartsWith OptionTypeName || ty.FullName.StartsWith ValueOptionTypeName then
ty.GetGenericArguments().[0]
Expand Down Expand Up @@ -172,12 +178,15 @@ module internal ReflectionHelper =
false

let actualFrom =
if from.FullName.StartsWith OptionTypeName || from.FullName.StartsWith ValueOptionTypeName then
if from.FullName.StartsWith OptionTypeName ||
from.FullName.StartsWith ValueOptionTypeName
then
from.GetGenericArguments()[0]
else from
let actualTo =
if ``to``.FullName.StartsWith OptionTypeName ||
``to``.FullName.StartsWith ValueOptionTypeName
``to``.FullName.StartsWith ValueOptionTypeName ||
``to``.FullName.StartsWith SkippableTypeName
then
``to``.GetGenericArguments()[0]
else ``to``
Expand Down
71 changes: 56 additions & 15 deletions src/FSharp.Data.GraphQL.Server/Values.fs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ let rec internal compileByType

let parametersMap =
let typeMismatchParameters = HashSet ()
let skippableMismatchParameters = HashSet ()
let nullableMismatchParameters = HashSet ()
let missingParameters = HashSet ()

Expand All @@ -123,22 +124,32 @@ let rec internal compileByType
|> Array.tryFind (fun field -> field.Name = param.Name)
with
| Some field ->
let isParameterSkippable = ReflectionHelper.isParameterSkippable param
match field.TypeDef with
| Nullable _ when field.IsSkippable <> isParameterSkippable ->
skippableMismatchParameters.Add param.Name |> ignore
| Nullable _ when
ReflectionHelper.isPrameterMandatory param
not (isParameterSkippable)
&& ReflectionHelper.isPrameterMandatory param
&& field.DefaultValue.IsNone
->
nullableMismatchParameters.Add param.Name |> ignore
| inputDef ->
let inputType, paramType = inputDef.Type, param.ParameterType
let inputType, paramType =
if isParameterSkippable then
inputDef.Type, param.ParameterType.GenericTypeArguments.[0]
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
| None ->
if ReflectionHelper.isParameterOptional param then
if
ReflectionHelper.isParameterSkippable param
|| ReflectionHelper.isParameterOptional param
then
allParameters.Add <| struct (ValueNone, param) |> ignore
else
missingParameters.Add param.Name |> ignore
Expand All @@ -157,6 +168,11 @@ let rec internal compileByType
let ``params`` = String.Join ("', '", nullableMismatchParameters)
$"Input object %s{objDef.Name} refers to type '%O{objtype}', but constructor parameters for optional GraphQL fields '%s{``params``}' are not optional"
InvalidInputTypeException (message, nullableMismatchParameters.ToImmutableHashSet ())
if skippableMismatchParameters.Any () then
let message =
let ``params`` = String.Join ("', '", skippableMismatchParameters)
$"Input object %s{objDef.Name} refers to type '%O{objtype}', but skippable '%s{``params``}' GraphQL fields and constructor parameters do not match"
InvalidInputTypeException (message, skippableMismatchParameters.ToImmutableHashSet ())
if typeMismatchParameters.Any () then
let message =
let ``params`` = String.Join ("', '", typeMismatchParameters)
Expand Down Expand Up @@ -204,15 +220,26 @@ let rec internal compileByType
parametersMap
|> Seq.map (fun struct (field, param) ->
match field with
| ValueSome field ->
match Map.tryFind field.Name props with
| None ->
Ok
<| wrapOptionalNone param.ParameterType field.TypeDef.Type
| Some prop ->
field.ExecuteInput prop variables
|> Result.map (normalizeOptional param.ParameterType)
|> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field
| 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``
else
return normalizeOptional param.ParameterType value
}
| ValueNone -> Ok <| wrapOptionalNone param.ParameterType typeof<obj>)
|> Seq.toList

Expand All @@ -236,12 +263,25 @@ let rec internal compileByType
parametersMap
|> Seq.map (fun struct (field, param) -> result {
match field with
| ValueSome field when field.IsSkippable && not (objectFields.ContainsKey field.Name) ->
return (Activator.CreateInstance param.ParameterType)
| ValueSome field ->
let! value =
field.ExecuteInput (VariableName field.Name) objectFields
// TODO: Take into account variable name
|> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field
return normalizeOptional param.ParameterType value
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 normalizedValue = normalizeOptional innerType value
let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType
return ``include`` normalizedValue
else
return normalizeOptional param.ParameterType value
| ValueNone -> return wrapOptionalNone param.ParameterType typeof<obj>
})
|> Seq.toList
Expand Down Expand Up @@ -506,6 +546,7 @@ and private coerceVariableInputObject inputObjectPath (originalObjDef, objDef) (
KeyValuePair (field.Name, value)
match input.TryGetProperty field.Name with
| true, value -> coerce value |> ValueSome
| false, _ when field.IsSkippable -> ValueNone
| false, _ ->
match field.DefaultValue with
| Some value -> KeyValuePair (field.Name, Ok value)
Expand Down
47 changes: 46 additions & 1 deletion src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
namespace FSharp.Data.GraphQL

open System
open System.Reflection
open System.Collections.Generic
open System.Collections.Immutable
open System.Reflection
open System.Text.Json.Serialization

/// General helper functions and types.
module Helpers =
Expand Down Expand Up @@ -133,3 +134,47 @@ module internal ReflectionHelper =
else input
else input
(some, none, value)

/// <summary>
/// Returns pair of function constructors for `include(value)` and `skip`
/// used to create option of type <paramref name="t"/> given at runtime.
/// </summary>
/// <param name="t">Type used for result option constructors as type param</param>
let ofSkippable (skippableType : Type) =
let skippableType = skippableType.GetTypeInfo ()
let skip =
let x = skippableType.GetDeclaredProperty "Skip"
x.GetValue(null)
let ``include`` =
let createInclude = skippableType.GetDeclaredMethod "NewInclude"
fun value ->
let valueType =
match value with
| null -> null
| _ -> value.GetType().GetTypeInfo()
if valueType = skippableType
then value
else createInclude.Invoke(null, [| value |])
(``include``, skip)

/// <summary>
/// Returns pair of function constructors for `include(value)` and `skip`
/// used to create option of type <paramref name="t"/> given at runtime.
/// </summary>
/// <param name="t">Type used for result option constructors as type param</param>
let skippableOfType t =
let skippableType = typedefof<_ Skippable>.GetTypeInfo().MakeGenericType([|t|]).GetTypeInfo()
let skip =
let x = skippableType.GetDeclaredProperty "Skip"
x.GetValue(null)
let ``include`` =
let createInclude = skippableType.GetDeclaredMethod "NewInclude"
fun value ->
let valueType =
match value with
| null -> null
| _ -> value.GetType().GetTypeInfo()
if valueType = skippableType
then value
else createInclude.Invoke(null, [| value |])
(``include``, skip)
24 changes: 24 additions & 0 deletions src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ module SchemaDefinitions =
Args =
[| { InputFieldDefinition.Name = "if"
Description = Some "Included when true."
IsSkippable = false
TypeDef = BooleanType
DefaultValue = None
ExecuteInput = variableOrElse (InlineConstant >> coerceBoolInput >> Result.map box) } |] }
Expand All @@ -492,6 +493,7 @@ module SchemaDefinitions =
Args =
[| { InputFieldDefinition.Name = "if"
Description = Some "Skipped when true."
IsSkippable = false
TypeDef = BooleanType
DefaultValue = None
ExecuteInput = variableOrElse (InlineConstant >> coerceBoolInput >> Result.map box) } |] }
Expand Down Expand Up @@ -1313,10 +1315,32 @@ module SchemaDefinitions =
static member Input(name : string, typedef : #InputDef<'In>, ?defaultValue : 'In, ?description : string) : InputFieldDef =
upcast { InputFieldDefinition.Name = name
Description = description
IsSkippable = false
TypeDef = typedef
DefaultValue = defaultValue
ExecuteInput = Unchecked.defaultof<ExecuteInput> }

/// <summary>
/// Creates an input field. Input fields are used like ordinary fileds in case of <see cref="InputObject"/>s,
/// and can be used to define arguments to objects and interfaces fields.
/// </summary>
/// <param name="name">
/// Field name. Must be unique in scope of the defining input object or withing field's argument list.
/// </param>
/// <param name="typedef">GraphQL type definition of the current input type</param>
/// <param name="defaultValue">If defined, this value will be used when no matching input has been provided by the requester.</param>
/// <param name="description">Optional input description. Usefull for generating documentation.</param>
static member SkippableInput(name : string, typedef : #InputDef<'In>, ?description : string) : InputFieldDef =
upcast { InputFieldDefinition.Name = name
Description = description |> Option.map (fun s -> s + " Skip this field if you want to avoid saving it")
IsSkippable = true
TypeDef =
match (box typedef) with
| :? NullableDef<'In> as n -> n
| _ -> Nullable typedef
DefaultValue = None
ExecuteInput = Unchecked.defaultof<ExecuteInput> }

/// <summary>
/// Creates a custom GraphQL interface type. It's needs to be implemented by object types and should not be used alone.
/// </summary>
Expand Down
Loading
Loading