From 1994f8b0ca2e3df63c22b7adaaf70e3abd3bbcd7 Mon Sep 17 00:00:00 2001 From: VectorTetra Date: Thu, 2 Jan 2025 15:21:40 +0200 Subject: [PATCH 1/2] Add tests for input records list --- .../FSharp.Data.GraphQL.Tests.fsproj | 1 + .../InputRecordListTests.fs | 170 ++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordListTests.fs diff --git a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj index c5613e69..10ef9d6c 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -55,6 +55,7 @@ + diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordListTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordListTests.fs new file mode 100644 index 00000000..40109457 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordListTests.fs @@ -0,0 +1,170 @@ +// The MIT License (MIT) + +module FSharp.Data.GraphQL.Tests.InputRecordListTests + +#nowarn "25" + +open Xunit +open System.Collections.Immutable +open System.Text.Json +open System.Text.Json.Serialization + +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Types +open FSharp.Data.GraphQL.Parser +open FSharp.Data.GraphQL.Tests.InputRecordTests + +let schema verify = + let schema = + Schema ( + query = + Define.Object ( + "Query", + [ Define.Field ( + "recordInputs", + StringType, + [ Define.Input ("record", ListOf InputRecordType) + Define.Input ("recordOptional",ListOf (Nullable InputRecordOptionalType)) + Define.Input ("recordNested",ListOf (Nullable InputRecordNestedType)) ], + (fun ctx name -> + let recordNested = ctx.Arg "recordNested" + match verify with + | Nothing -> () + | AllInclude -> recordNested.s |> ValueOption.iter _.VerifyAllInclude + | AllSkip -> recordNested.s |> ValueOption.iter _.VerifyAllSkip + | SkipAndIncludeNull -> recordNested.s |> ValueOption.iter _.VerifySkipAndIncludeNull + stringifyInput ctx name + ) + ) // TODO: add all args stringificaiton + Define.Field ( + "objectInputs", + StringType, + [ Define.Input ("object", InputObjectType) + Define.Input ("objectOptional", Nullable InputObjectOptionalType) ], + stringifyInput + ) ] // TODO: add all args stringificaiton + ) + ) + + Executor schema + +[] +let ``Execute handles creation of inline empty input records list`` () = + let query = + """{ + recordInputs( + record: [], + recordOptional: [], + recordNested: [] + ) + }""" + let result = sync <| (schema AllInclude).AsyncExecute(parse query) + ensureDirect result <| fun data errors -> empty errors + +[] +let ``Execute handles creation of inline input records list with all fields`` () = + let query = + """{ + recordInputs( + record: [{ a: "a", b: "b", c: "c" }], + recordOptional: [{ a: "a", b: "b", c: "c" }], + recordNested: [ + a: { a: "a", b: "b", c: "c" }, + b: { a: "a", b: "b", c: "c" }, + c: { a: "a", b: "b", c: "c" }, + s: { a: "a", b: "b", c: "c" }, + l: [{ a: "a", b: "b", c: "c" }] + ] + ) + }""" + let result = sync <| (schema AllInclude).AsyncExecute(parse query) + ensureDirect result <| fun data errors -> empty errors + +[] +let ``Execute handles creation of inline input records list with optional null fields`` () = + let query = + """{ + recordInputs( + record: [{ a: "a", b: "b", c: "c" }], + recordOptional: [null], + recordNested: [{ a: { a: "a", b: "b", c: "c" }, b: null, c: null, s: null, l: [] }] + ) + }""" + let result = sync <| (schema Nothing).AsyncExecute(parse query) + ensureDirect result <| fun data errors -> empty errors + +[] +let ``Execute handles creation of inline input records list with mandatory only fields`` () = + let query = + """{ + recordInputs( + record: [{ a: "a", b: "b", c: "c" }], + recordNested: [{ a: { a: "a", b: "b", c: "c" }, l: [{ a: "a", b: "b", c: "c" }] }] + ) + }""" + let result = sync <| (schema Nothing).AsyncExecute(parse query) + ensureDirect result <| fun data errors -> empty errors + +let variablesWithAllInputs (record, optRecord, skippable) = + $""" + [{{ + "record":%s{record}, + "optRecord":%s{optRecord}, + "skippable": %s{skippable}, + "list":[%s{record}] + }}] +""" + +let paramsWithValues variables = + JsonDocument + .Parse(variables : string) + .RootElement.Deserialize> (serializerOptions) + +[] +let ``Execute handles creation of input records list from variables with all fields`` () = + let query = + """query ($record: InputRecord!, $optRecord: InputRecordOptional, $skippable: InputRecordSkippable, $list: [InputRecord!]!){ + recordInputs( + record: $record, + recordOptional: $optRecord, + recordNested: [{ a: $record, b: $optRecord, c: $optRecord, s: $skippable, l: $list }] + ) + }""" + let testInputObject = """{"a":"a","b":"b","c":"c"}""" + let params' = + variablesWithAllInputs(testInputObject, testInputObject, testInputObject) |> paramsWithValues + let result = sync <| (schema AllInclude).AsyncExecute(parse query, variables = params') + //let expected = NameValueLookup.ofList [ "recordInputs", upcast testInputObject ] + ensureDirect result <| fun data errors -> + empty errors + //data |> equals (upcast expected) + +[] +let ``Execute handles creation of input records list from variables with optional null fields`` () = + let query = + """query ($record: InputRecord!, $optRecord: InputRecordOptional, $skippable: InputRecordSkippable, $list: [InputRecord!]!){ + recordInputs( + record: $record, + recordOptional: $optRecord, + recordNested: [{ a: $record, b: $optRecord, c: $optRecord, s: $skippable, l: $list }] + ) + }""" + let testInputObject = """{"a":"a","b":"b","c":"c"}""" + let testInputSkippable = """{ "a": null, "b": null, "c": null }""" + let params' = variablesWithAllInputs(testInputObject, "null", testInputSkippable) |> paramsWithValues + let result = sync <| (schema SkipAndIncludeNull).AsyncExecute(parse query, variables = params') + ensureDirect result <| fun data errors -> empty errors + +[] +let ``Execute handles creation of input records from variables with mandatory only fields`` () = + let query = + """query ($record: InputRecord!, $list: [InputRecord!]!){ + recordInputs( + record: $record, + recordNested: [{ a: $record, l: $list }] + ) + }""" + let testInputObject = """{"a":"a","b":"b","c":"c"}""" + let params' = variablesWithAllInputs(testInputObject, "null", "{}") |> paramsWithValues + let result = sync <| (schema AllSkip).AsyncExecute(parse query, variables = params') + ensureDirect result <| fun data errors -> empty errors From 807d20c57faeace0a0043ab6d06b60a92fc57f29 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 2 Jan 2025 21:32:40 +0400 Subject: [PATCH 2/2] Fixed crash of input objects list creation from variables --- src/FSharp.Data.GraphQL.Server/Values.fs | 7 ++ .../InputRecordListTests.fs | 97 +++++++++++-------- 2 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index 4a4af46a..8f93e8bf 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -317,6 +317,13 @@ let rec internal compileByType | _ -> Ok null | List (Input innerDef) -> + match innerDef with + | InputObject inputObjDef + | Nullable (InputObject inputObjDef) -> + let inner = compileByType inputObjectPath inputSource (inputDef, innerDef) + inputObjDef.ExecuteInput <- inner + | _ -> () + let isArray = inputDef.Type.IsArray // TODO: Improve creation of inner let inner index = compileByType ((box index) :: inputObjectPath) inputSource (innerDef, innerDef) diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordListTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordListTests.fs index 40109457..149005f0 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordListTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordListTests.fs @@ -23,16 +23,22 @@ let schema verify = [ Define.Field ( "recordInputs", StringType, - [ Define.Input ("record", ListOf InputRecordType) - Define.Input ("recordOptional",ListOf (Nullable InputRecordOptionalType)) - Define.Input ("recordNested",ListOf (Nullable InputRecordNestedType)) ], + [ Define.Input ("records", ListOf InputRecordType) + Define.Input ("recordsOptional", Nullable (ListOf (Nullable InputRecordOptionalType))) + Define.Input ("recordsNested", ListOf (Nullable InputRecordNestedType)) ], (fun ctx name -> - let recordNested = ctx.Arg "recordNested" - match verify with - | Nothing -> () - | AllInclude -> recordNested.s |> ValueOption.iter _.VerifyAllInclude - | AllSkip -> recordNested.s |> ValueOption.iter _.VerifyAllSkip - | SkipAndIncludeNull -> recordNested.s |> ValueOption.iter _.VerifySkipAndIncludeNull + let _ = ctx.Arg "records" + let _ = ctx.TryArg> "recordsOptional" + let recordNested = + ctx.Arg> "recordsNested" + |> List.tryHead + |> Option.flatten + match verify, recordNested with + | Nothing, _ -> () + | AllInclude, Some recordNested -> recordNested.s |> ValueOption.iter _.VerifyAllInclude + | AllSkip, Some recordNested -> recordNested.s |> ValueOption.iter _.VerifyAllSkip + | SkipAndIncludeNull, Some recordNested -> recordNested.s |> ValueOption.iter _.VerifySkipAndIncludeNull + | _ -> () stringifyInput ctx name ) ) // TODO: add all args stringificaiton @@ -53,9 +59,9 @@ let ``Execute handles creation of inline empty input records list`` () = let query = """{ recordInputs( - record: [], - recordOptional: [], - recordNested: [] + records: [], + recordsOptional: [], + recordsNested: [] ) }""" let result = sync <| (schema AllInclude).AsyncExecute(parse query) @@ -66,15 +72,15 @@ let ``Execute handles creation of inline input records list with all fields`` () let query = """{ recordInputs( - record: [{ a: "a", b: "b", c: "c" }], - recordOptional: [{ a: "a", b: "b", c: "c" }], - recordNested: [ + records: [{ a: "a", b: "b", c: "c" }], + recordsOptional: [{ a: "a", b: "b", c: "c" }], + recordsNested: [{ a: { a: "a", b: "b", c: "c" }, b: { a: "a", b: "b", c: "c" }, c: { a: "a", b: "b", c: "c" }, s: { a: "a", b: "b", c: "c" }, l: [{ a: "a", b: "b", c: "c" }] - ] + }] ) }""" let result = sync <| (schema AllInclude).AsyncExecute(parse query) @@ -85,9 +91,9 @@ let ``Execute handles creation of inline input records list with optional null f let query = """{ recordInputs( - record: [{ a: "a", b: "b", c: "c" }], - recordOptional: [null], - recordNested: [{ a: { a: "a", b: "b", c: "c" }, b: null, c: null, s: null, l: [] }] + records: [{ a: "a", b: "b", c: "c" }], + recordsOptional: [null], + recordsNested: [{ a: { a: "a", b: "b", c: "c" }, b: null, c: null, s: null, l: [] }] ) }""" let result = sync <| (schema Nothing).AsyncExecute(parse query) @@ -98,8 +104,8 @@ let ``Execute handles creation of inline input records list with mandatory only let query = """{ recordInputs( - record: [{ a: "a", b: "b", c: "c" }], - recordNested: [{ a: { a: "a", b: "b", c: "c" }, l: [{ a: "a", b: "b", c: "c" }] }] + records: [{ a: "a", b: "b", c: "c" }], + recordsNested: [{ a: { a: "a", b: "b", c: "c" }, l: [{ a: "a", b: "b", c: "c" }] }] ) }""" let result = sync <| (schema Nothing).AsyncExecute(parse query) @@ -107,12 +113,11 @@ let ``Execute handles creation of inline input records list with mandatory only let variablesWithAllInputs (record, optRecord, skippable) = $""" - [{{ - "record":%s{record}, - "optRecord":%s{optRecord}, - "skippable": %s{skippable}, - "list":[%s{record}] - }}] + {{ + "records":[%s{record}], + "optRecords":[%s{optRecord}], + "nestedRecords":[ {{ "a": {record}, "b": {optRecord}, "c": {optRecord}, "s": {skippable}, "l": [{record}] }}] + }} """ let paramsWithValues variables = @@ -123,11 +128,15 @@ let paramsWithValues variables = [] let ``Execute handles creation of input records list from variables with all fields`` () = let query = - """query ($record: InputRecord!, $optRecord: InputRecordOptional, $skippable: InputRecordSkippable, $list: [InputRecord!]!){ + """query ( + $records: [InputRecord!]!, + $optRecords: [InputRecordOptional], + $nestedRecords: [InputRecordNested]! + ) { recordInputs( - record: $record, - recordOptional: $optRecord, - recordNested: [{ a: $record, b: $optRecord, c: $optRecord, s: $skippable, l: $list }] + records: $records, + recordsOptional: $optRecords, + recordsNested: $nestedRecords ) }""" let testInputObject = """{"a":"a","b":"b","c":"c"}""" @@ -142,11 +151,15 @@ let ``Execute handles creation of input records list from variables with all fie [] let ``Execute handles creation of input records list from variables with optional null fields`` () = let query = - """query ($record: InputRecord!, $optRecord: InputRecordOptional, $skippable: InputRecordSkippable, $list: [InputRecord!]!){ + """query ( + $records: [InputRecord!]!, + $optRecords: [InputRecordOptional], + $nestedRecords: [InputRecordNested]! + ) { recordInputs( - record: $record, - recordOptional: $optRecord, - recordNested: [{ a: $record, b: $optRecord, c: $optRecord, s: $skippable, l: $list }] + records: $records, + recordsOptional: $optRecords, + recordsNested: $nestedRecords ) }""" let testInputObject = """{"a":"a","b":"b","c":"c"}""" @@ -157,14 +170,22 @@ let ``Execute handles creation of input records list from variables with optiona [] let ``Execute handles creation of input records from variables with mandatory only fields`` () = + let variablesWithAllInputs (record) = + $""" + {{ + "record":%s{record}, + "list":[%s{record}] + }} + """ + let query = """query ($record: InputRecord!, $list: [InputRecord!]!){ recordInputs( - record: $record, - recordNested: [{ a: $record, l: $list }] + records: [$record], + recordsNested: [{ a: $record, l: $list }] ) }""" let testInputObject = """{"a":"a","b":"b","c":"c"}""" - let params' = variablesWithAllInputs(testInputObject, "null", "{}") |> paramsWithValues + let params' = variablesWithAllInputs testInputObject |> paramsWithValues let result = sync <| (schema AllSkip).AsyncExecute(parse query, variables = params') ensureDirect result <| fun data errors -> empty errors