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

Nested InputObjects don't get populated #472

Open
pkese opened this issue Apr 6, 2024 · 10 comments
Open

Nested InputObjects don't get populated #472

pkese opened this issue Apr 6, 2024 · 10 comments

Comments

@pkese
Copy link
Contributor

pkese commented Apr 6, 2024

I've added a small random generator query to star-wars-api example:

    let randoms =
        let inputs = [
            Define.Input ("count", IntType)
            Define.Input ("range", Nullable (Define.InputObject("RandomRange", [
                Define.Input ("min", IntType)
                Define.Input ("max", IntType)
            ])))
        ]
        let renderRandoms (ctx:ResolveFieldContext) (r:Root) =
            // printfn "Random called with count:%A and range:%A" (ctx.Arg "count") (ctx.Arg "range")
            // todo: ranges are not implemented, because ctx.Arg("range") is always empty (read below)
            [ for i in 1..ctx.Arg("count") -> System.Random.Shared.Next() ]

        Define.Field ("random", ListOf IntType, "Render random numbers", inputs, renderRandoms)

and plugged it into existing Query:

    let Query =
        let inputs = [ Define.Input ("id", StringType) ]
        Define.Object<Root> (
            name = "Query",
            fields =
                [ Define.Field ("hero", Nullable HumanType, "Gets human hero", inputs, fun ctx _ -> getHuman (ctx.Arg ("id")))
                  Define.Field ("droid", Nullable DroidType, "Gets droid", inputs, (fun ctx _ -> getDroid (ctx.Arg ("id"))))
                  Define.Field ("planet", Nullable PlanetType, "Gets planet", inputs, fun ctx _ -> getPlanet (ctx.Arg ("id")))
                  Define.Field ("characters", ListOf CharacterType, "Gets characters", (fun _ _ -> characters))
                  randoms // <-- here
                ])

Now I can say

query { random(count:5) }

and get

{ "documentId": -804090008,
  "data": {
    "random": [1773619870, 512090916, 1972640319, 301711584, 480788316]
}}

Problem

When I call

query { random(count:5, range:{min:10, max:20}) }

The ctx.args("range") is always passed as an empty unpopulated obj() without any type or properties,
whereas ctx.args("count") works as expected.

@xperiandri
Copy link
Collaborator

xperiandri commented Apr 6, 2024

That happens because you need to declare

type Range = { Min: int; Max: int }
let inputs = [
    Define.Input ("count", IntType)
        Define.Input ("range", Nullable (Define.InputObject<Range>("RandomRange", [
            Define.Input ("min", IntType)
            Define.Input ("max", IntType)
        ])))
    ]

As Define.InputObject() equals to Define.InputObject<obj>().
There is no functionality to deserialize into IReadOnlyDictionary<string, obj> if no type parameter is specified.

@xperiandri
Copy link
Collaborator

There are 2 options to fix Define.InputObject<obj>:

  1. Exception at runtime when schema is created that it is not allowed
  2. Deserialize into IReadOnlyDictionary<string, obj>

@pkese
Copy link
Contributor Author

pkese commented Apr 6, 2024

@xperiandri thanks, I appreciate your help.

Defining a Range type works.
My problem however is that in my real app, I don't have a fixed input type known a compile time (available input values depend on dynamic database schema).

I suppose I could dynamically create a new type and then use reflection to call the Define.InputObject with proper generic instantiation with that dynamic type (is this the right path or is there an easier workarond)?

A nice option (from the library side) besides IReadOnlyDictionary<string, obj> would be to allow Define.InputObject<JsonElement>. Probably anything that can be expressed as an input expression should be 1:1 mappable to Json.

@xperiandri
Copy link
Collaborator

Have you tried that? Doesn’t it work?

@pkese
Copy link
Contributor Author

pkese commented Apr 6, 2024

I've tried with

    let randoms =
        let inputs = [
            Define.Input ("count", IntType)
            Define.Input ("range", Nullable (Define.InputObject<System.Text.Json.JsonElement>("Range", [
                Define.Input ("min", IntType)
                Define.Input ("max", IntType)
            ])))
        ]
        let renderRandoms (ctx:ResolveFieldContext) (r:Root) =
            [ for i in 1..ctx.Arg("count") -> System.Random.Shared.Next() ]

        Define.Field ("random", ListOf IntType, "Render random numbers", inputs, renderRandoms)

... but it's resulting in a runtime error when the app starts:

Unhandled exception. System.TypeInitializationException: The type initializer for 'FSharp.Data.GraphQL.Samples.StarWarsApi.Schema' threw an exception.
 ---> System.TypeInitializationException: The type initializer for '<StartupCode$FSharp-Data-GraphQL-Samples-StarWarsApi>.$Schema' threw an exception.
 ---> System.Collections.Generic.KeyNotFoundException: An index satisfying the predicate was not found in the collection.
   at Microsoft.FSharp.Collections.SeqModule.Find[T](FSharpFunc`2 predicate, IEnumerable`1 source) in D:\a\_work\1\s\src\FSharp.Core\seq.fs:line 840
   at FSharp.Data.GraphQL.ReflectionHelper.matchConstructor(Type t, String[] fields) in /home/peter/work/org/FSharp.Data.GraphQL/src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs:line 168
   at FSharp.Data.GraphQL.Values.compileByType(FSharpList`1 inputObjectPath, InputSource inputSource, InputDef originalInputDef, InputDef inputDef) in /home/peter/work/org/FSharp.Data.GraphQL/src/FSharp.Data.GraphQL.Server/Values.fs:line 92
   at FSharp.Data.GraphQL.Values.compileByType(FSharpList`1 inputObjectPath, InputSource inputSource, InputDef originalInputDef, InputDef inputDef) in /home/peter/work/org/FSharp.Data.GraphQL/src/FSharp.Data.GraphQL.Server/Values.fs:line 255
   at [email protected](String _arg1, FieldDef fieldDef) in /home/peter/work/org/FSharp.Data.GraphQL/src/FSharp.Data.GraphQL.Server/Execution.fs:line 672
   at Microsoft.FSharp.Collections.MapTreeModule.iterOpt[TKey,TValue](FSharpFunc`3 f, MapTree`2 m) in D:\a\_work\1\s\src\FSharp.Core\map.fs:line 358
   at FSharp.Data.GraphQL.Execution.compileObject(ObjectDef objdef, FSharpFunc`2 executeFields) in /home/peter/work/org/FSharp.Data.GraphQL/src/FSharp.Data.GraphQL.Server/Execution.fs:line 665
   at [email protected](Tuple`2 tupledArg) in /home/peter/work/org/FSharp.Data.GraphQL/src/FSharp.Data.GraphQL.Server/Execution.fs:line 691
   at Microsoft.FSharp.Collections.SeqModule.Iterate[T](FSharpFunc`2 action, IEnumerable`1 source) in D:\a\_work\1\s\src\FSharp.Core\seq.fs:line 632
   at FSharp.Data.GraphQL.Execution.compileSchema(SchemaCompileContext ctx) in /home/peter/work/org/FSharp.Data.GraphQL/src/FSharp.Data.GraphQL.Server/Execution.fs:line 681
   at <StartupCode$FSharp-Data-GraphQL-Server>[email protected](SchemaCompileContext ctx)
   at <StartupCode$FSharp-Data-GraphQL-Server>[email protected](ctx ctx, FSharpList`1 middlewares) in /home/peter/work/org/FSharp.Data.GraphQL/src/FSharp.Data.GraphQL.Server/Executor.fs:line 86
   at <StartupCode$FSharp-Data-GraphQL-Server>[email protected](ctx ctx') in /home/peter/work/org/FSharp.Data.GraphQL/src/FSharp.Data.GraphQL.Server/Executor.fs:line 89
   at FSharp.Data.GraphQL.Server.Middleware.LiveQueryMiddleware.middleware(SchemaCompileContext ctx, FSharpFunc`2 next) in /home/peter/work/org/FSharp.Data.GraphQL/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs:line 167
  ...

@xperiandri
Copy link
Collaborator

What about Dictionary?

@xperiandri
Copy link
Collaborator

Or dynamic

@pkese
Copy link
Contributor Author

pkese commented Apr 6, 2024

Define.InputObject<System.Collections.Generic.Dictionary<string,obj>> compiles fine but at query time results in ctx.Arg("range") being an empty sequence.

Similarly Define.InputObject<System.Dynamic.DynamicObject> compiles but at runtime returns a Dynamic object with no members.

@xperiandri
Copy link
Collaborator

Thanks for testing!
The input object logic is here
We can check if the type is some known dynamic type or object then switch to a dictionary logic

pkese added a commit to pkese/FSharp.Data.GraphQL that referenced this issue Apr 6, 2024
@pkese
Copy link
Contributor Author

pkese commented Apr 6, 2024

Thanks @xperiandri, based on your hints, I managed to patch the code in #475 ... and that fixes my issue.

(ignore the link just above this comment: I somehow managed to commit too many files into the pool request, so I closed that request and made a new one).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants