From 66b743b6488a000bd5723f103d98ca418ec6970b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 19:49:59 +0200 Subject: [PATCH 01/32] Migrate integration tests to in-process hosts via `WebApplicationFactory` and remove external server orchestration from build (#564) Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Andrii Chebukin --- FSharp.Data.GraphQL.Integration.slnx | 5 + Packages.props | 1 + build/Program.fs | 72 +- .../star-wars-fabulous-client/StarWars.slnx | 6 + .../StarWars/Common.fs | 5 +- .../StarWars/StarWars.fsproj | 20 +- ...Sharp.Data.GraphQL.IntegrationTests.fsproj | 17 +- .../IntrospectionUpdateTests.fs | 85 + .../LocalProviderTests.fs | 611 ++++-- ...ProviderWithOptionalParametersOnlyTests.fs | 607 ++++-- .../OperationErrorTests.fs | 10 +- .../ReservedScalarNameProviderTests.fs | 16 +- .../SwapiLocalProviderTests.fs | 145 +- .../SwapiRemoteProviderTests.fs | 167 +- .../TestHosts.fs | 41 + .../integration-introspection.json | 1929 +++++++++++++++++ .../introspection.json | 2 +- 17 files changed, 3111 insertions(+), 628 deletions(-) create mode 100644 tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs create mode 100644 tests/FSharp.Data.GraphQL.IntegrationTests/TestHosts.fs create mode 100644 tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json diff --git a/FSharp.Data.GraphQL.Integration.slnx b/FSharp.Data.GraphQL.Integration.slnx index ebd077769..b8e081034 100644 --- a/FSharp.Data.GraphQL.Integration.slnx +++ b/FSharp.Data.GraphQL.Integration.slnx @@ -4,6 +4,9 @@ + + + @@ -12,7 +15,9 @@ + + diff --git a/Packages.props b/Packages.props index 7d3b9582b..323f603f2 100644 --- a/Packages.props +++ b/Packages.props @@ -76,6 +76,7 @@ + diff --git a/build/Program.fs b/build/Program.fs index 22bbb133c..f23d68415 100644 --- a/build/Program.fs +++ b/build/Program.fs @@ -2,8 +2,6 @@ module Program open System open System.IO -open System.Net.Http -open System.Text.Json open Fake.Core open Fake.Core.TargetOperators @@ -123,26 +121,6 @@ let runTests (project : string) (args : string) = |> _.WithCommon(DotNetCli.setVersion)) project -let starWarsServerStream = StreamRef.Empty - -let [] StartStarWarsServerTarget = "StartStarWarsServer" -Target.create StartStarWarsServerTarget <| fun _ -> - Target.activateFinal "StopStarWarsServer" - - let project = - "samples" - "star-wars-api" - "star-wars-api.fsproj" - - startGraphQLServer project 8086 starWarsServerStream - -let [] StopStarWarsServerTarget = "StopStarWarsServer" -Target.createFinal StopStarWarsServerTarget <| fun _ -> - try - starWarsServerStream.Value.Write ([| 0uy |], 0, 1) - with e -> - printfn "%s" e.Message - let integrationTestServerProjectPath = "tests" "FSharp.Data.GraphQL.IntegrationTests.Server" @@ -179,58 +157,35 @@ Target.createFinal StopIntegrationServerTarget <| fun _ -> with e -> printfn "%s" e.Message -let [] UpdateIntrospectionFileTarget = "UpdateIntrospectionFile" -Target.create UpdateIntrospectionFileTarget <| fun _ -> - let client = new HttpClient () - (task { - let! result = client.GetAsync ("http://localhost:8086") - let! contentStream = result.Content.ReadAsStreamAsync () - let! jsonDocument = JsonDocument.ParseAsync contentStream - let file = - new FileStream ("tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json", FileMode.Create, FileAccess.Write, FileShare.None) - let encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping - let jsonWriterOptions = JsonWriterOptions (Indented = true, Encoder = encoder) - let writer = new Utf8JsonWriter (file, jsonWriterOptions) - jsonDocument.WriteTo writer - do! writer.FlushAsync () - do! writer.DisposeAsync () - do! file.DisposeAsync () - result.Dispose () - }) - .Wait () - client.Dispose () - -let unitTestsProjectPath = - "tests" - "FSharp.Data.GraphQL.Tests" - "FSharp.Data.GraphQL.Tests.fsproj" - let integrationTestsProjectPath = "tests" "FSharp.Data.GraphQL.IntegrationTests" "FSharp.Data.GraphQL.IntegrationTests.fsproj" -let [] BuildIntegrationTestsTarget = "BuildIntegrationTests" -Target.create BuildIntegrationTestsTarget <| fun _ -> +let [] UpdateIntrospectionFileTarget = "UpdateIntrospectionFile" +Target.create UpdateIntrospectionFileTarget <| fun _ -> integrationTestsProjectPath - |> DotNet.build (fun options -> { + |> DotNet.test (fun options -> { options with + Framework = Some DotNetMoniker Configuration = configuration + Common = { DotNetCli.setVersion options.Common with CustomParams = Some "--filter FullyQualifiedName~IntrospectionUpdateTests" } MSBuildParams = { options.MSBuildParams with DisableInternalBinLog = true + Verbosity = Some Normal } - Common = DotNetCli.setVersion options.Common }) +let unitTestsProjectPath = + "tests" + "FSharp.Data.GraphQL.Tests" + "FSharp.Data.GraphQL.Tests.fsproj" + let [] RunUnitTestsTarget = "RunUnitTests" Target.create RunUnitTestsTarget <| fun _ -> runTests unitTestsProjectPath "" -let [] RunIntegrationTestsTarget = "RunIntegrationTests" -Target.create RunIntegrationTestsTarget <| fun _ -> - runTests integrationTestsProjectPath "" //"--filter Execution=Sync" - let prepareDocGen () = Shell.rm "docs/release-notes.md" Shell.cp "RELEASE_NOTES.md" "docs/RELEASE_NOTES.md" @@ -406,12 +361,7 @@ Target.create "PackAndPush" ignore ==> RestoreTarget ==> BuildTarget ==> RunUnitTestsTarget -==> StartStarWarsServerTarget -==> BuildIntegrationTestServerTarget -==> StartIntegrationServerTarget ==> UpdateIntrospectionFileTarget -==> BuildIntegrationTestsTarget -==> RunIntegrationTestsTarget ==> "All" =?> (GenerateDocsTarget, Environment.environVar "GITHUB_ACTIONS" = "True") |> ignore diff --git a/samples/star-wars-fabulous-client/StarWars.slnx b/samples/star-wars-fabulous-client/StarWars.slnx index 51d68264a..52cff01ec 100644 --- a/samples/star-wars-fabulous-client/StarWars.slnx +++ b/samples/star-wars-fabulous-client/StarWars.slnx @@ -14,6 +14,12 @@ + + + + + + diff --git a/samples/star-wars-fabulous-client/StarWars/Common.fs b/samples/star-wars-fabulous-client/StarWars/Common.fs index 3974e6c92..1de5ae4e9 100644 --- a/samples/star-wars-fabulous-client/StarWars/Common.fs +++ b/samples/star-wars-fabulous-client/StarWars/Common.fs @@ -5,7 +5,10 @@ open FSharp.Data.GraphQL module Commands = - type GraphQLApi = GraphQLProvider<"http://localhost:8086"> + [] + let IntrospectionPath = "../../../tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json" + + type GraphQLApi = GraphQLProvider let GetCharactersData = GraphQLApi.Operation<"queries/FetchCharacters.graphql">() type Character = GraphQLApi.Operations.FetchCharacters.Types.CharactersFields.Character diff --git a/samples/star-wars-fabulous-client/StarWars/StarWars.fsproj b/samples/star-wars-fabulous-client/StarWars/StarWars.fsproj index d972d6d59..4769849ad 100644 --- a/samples/star-wars-fabulous-client/StarWars/StarWars.fsproj +++ b/samples/star-wars-fabulous-client/StarWars/StarWars.fsproj @@ -4,7 +4,7 @@ false - + @@ -18,16 +18,16 @@ - - - - - - - - + + + + + + + + - \ No newline at end of file + diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj b/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj index f133dee2a..5d7e2d004 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj @@ -7,6 +7,7 @@ + @@ -15,6 +16,7 @@ + @@ -22,7 +24,9 @@ + + @@ -30,16 +34,19 @@ + + + + + - ..\..\src\FSharp.Data.GraphQL.Client\bin\Debug\netstandard2.0\FSharp.Data.GraphQL.Client.dll - ..\..\src\FSharp.Data.GraphQL.Client\bin\Release\netstandard2.0\FSharp.Data.GraphQL.Client.dll + ..\..\src\FSharp.Data.GraphQL.Client\bin\$(Configuration)\netstandard2.0\FSharp.Data.GraphQL.Client.dll ..\..\bin\FSharp.Data.GraphQL.Client\netstandard2.0\FSharp.Data.GraphQL.Client.dll - ..\..\src\FSharp.Data.GraphQL.Client\bin\Debug\netstandard2.0\FSharp.Data.GraphQL.Shared.dll - ..\..\src\FSharp.Data.GraphQL.Client\bin\Release\netstandard2.0\FSharp.Data.GraphQL.Shared.dll - ..\..\bin\FSharp.Data.GraphQL.Client\netstandard2.0\FSharp.Data.GraphQL.Shared.dll + ..\..\src\FSharp.Data.GraphQL.Shared\bin\$(Configuration)\net10.0\FSharp.Data.GraphQL.Shared.dll + ..\..\bin\FSharp.Data.GraphQL.Shared\net10.0\FSharp.Data.GraphQL.Shared.dll diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs new file mode 100644 index 000000000..c3931b295 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs @@ -0,0 +1,85 @@ +module FSharp.Data.GraphQL.IntegrationTests.IntrospectionUpdateTests + +open System +open System.IO +open System.Net.Http.Json +open System.Text.Json +open System.Threading +open Xunit + +let introspectionFilePath = + Path.Combine (__SOURCE_DIRECTORY__, "integration-introspection.json") + |> Path.GetFullPath + +let normalizeJsonDocument options (document : JsonDocument) = + use buffer = new MemoryStream () + use writer = new Utf8JsonWriter (buffer, options) + document.WriteTo writer + writer.Flush () + buffer.Seek (0L, SeekOrigin.Begin) |> ignore + JsonDocument.Parse buffer + +let parseAndNormalizeJsonAsync ct options stream = task { + let! document = JsonDocument.ParseAsync (stream, cancellationToken = ct) + return normalizeJsonDocument options document +} + +let areSchemasEqual (document1 : JsonDocument) (document2 : JsonDocument) = + let schema1 = document1.RootElement.GetProperty("data").GetProperty ("__schema") + let schema2 = document2.RootElement.GetProperty("data").GetProperty ("__schema") + schema1.GetRawText () = schema2.GetRawText () + +let readDestinationDocumentAsync ct (stream : FileStream) = task { + try + let! document = JsonDocument.ParseAsync (stream, cancellationToken = ct) + return ValueSome document + with :? JsonException -> + return ValueNone +} + +let updateIntrospectionFileAsync ct sourceStream = task { + use destinationStream = + new FileStream (introspectionFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read) + + let options = JsonWriterOptions (Indented = true) + let! sourceDocument = parseAndNormalizeJsonAsync ct options sourceStream + destinationStream.Seek (0L, SeekOrigin.Begin) |> ignore + let! destinationDocument = readDestinationDocumentAsync ct destinationStream + + let shouldUpdate = + match destinationDocument with + | ValueNone -> true + | ValueSome document -> not (areSchemasEqual document sourceDocument) + + if shouldUpdate then + destinationStream.Seek (0L, SeekOrigin.Begin) |> ignore + destinationStream.SetLength 0 + use writer = new Utf8JsonWriter (destinationStream, options) + sourceDocument.WriteTo writer + writer.Flush () + + return shouldUpdate +} + +[] +let ``Get GraphQL introspection response returns schema`` () = task { + use httpClient = TestHosts.createIntegrationHttpClient () + let! response = httpClient.GetFromJsonAsync ("/", CancellationToken.None) + let schema = response.GetProperty("data").GetProperty ("__schema") + Assert.NotEqual (Unchecked.defaultof, schema) + let hasErrors, _ = response.TryGetProperty "errors" + Assert.False hasErrors +} + +[] +let ``Update integration introspection file when schema changes`` () = task { + use httpClient = TestHosts.createIntegrationHttpClient () + let! sourceStream = httpClient.GetStreamAsync ("/") + let! wasUpdated = updateIntrospectionFileAsync CancellationToken.None sourceStream + Assert.True (File.Exists introspectionFilePath) + if wasUpdated then + let! sourceStreamSecondRun = httpClient.GetStreamAsync ("/") + use sourceStreamForVerification = sourceStreamSecondRun + let! wasUpdatedSecondRun = updateIntrospectionFileAsync CancellationToken.None sourceStreamForVerification + Assert.False wasUpdatedSecondRun +} diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs index 897364cb1..d1a7381fd 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs @@ -5,13 +5,17 @@ open System.Threading.Tasks open FSharp.Data.GraphQL open Helpers -let [] ServerUrl = "http://localhost:8085" -let [] EmptyGuidAsString = "00000000-0000-0000-0000-000000000000" +[] +let IntrospectionPath = "integration-introspection.json" +[] +let EmptyGuidAsString = "00000000-0000-0000-0000-000000000000" -type Provider = GraphQLProvider +type Provider = GraphQLProvider // type FileProvider = GraphQLProvider -let context = Provider.GetContext(ServerUrl) +let connection = TestHosts.createIntegrationConnection () +let context = + Provider.GetContext (serverUrl = TestHosts.integrationServerUrl, connectionFactory = (fun () -> connection)) type Input = Provider.Types.Input type InputField = Provider.Types.InputField @@ -37,165 +41,175 @@ module SimpleOperation = uri deprecated guid - }""">() + }"""> () type Operation = Provider.Operations.Q let validateResult (input : Input option) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Classic" result.Data.IsSome |> equals true - input |> Option.iter (fun input -> + input + |> Option.iter (fun input -> result.Data.Value.Echo.IsSome |> equals true - input.List |> Option.iter (fun list -> + input.List + |> Option.iter (fun list -> result.Data.Value.Echo.Value.List.IsSome |> equals true - let input = list |> Array.map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid.ToString()) - let output = result.Data.Value.Echo.Value.List.Value |> Array.map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid) + let input = + list + |> Array.map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid.ToString ()) + let output = + result.Data.Value.Echo.Value.List.Value + |> Array.map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid) input |> equals output) - input.Single |> Option.iter (fun single -> + input.Single + |> Option.iter (fun single -> result.Data.Value.Echo.Value.Single.IsSome |> equals true - let input = single.Int, single.IntOption, single.String, single.StringOption, single.Uri, single.Guid.ToString() - let output = result.Data.Value.Echo.Value.Single.Value |> map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid) + let input = + single.Int, single.IntOption, single.String, single.StringOption, single.Uri, single.Guid.ToString () + let output = + result.Data.Value.Echo.Value.Single.Value + |> map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid) input |> equals output)) [] -let ``Should be able to execute a query without sending input field``() = - SimpleOperation.operation.Run() +let ``Should be able to execute a query without sending input field`` () = + SimpleOperation.operation.Run (context) |> SimpleOperation.validateResult None [] -let ``Should be able to execute a query using context, without sending input field``() = - SimpleOperation.operation.Run(context) +let ``Should be able to execute a query using context, without sending input field`` () = + SimpleOperation.operation.Run (context) |> SimpleOperation.validateResult None [] -let ``Should be able to execute a query without sending input field asynchronously``() : Task = task { - let! result = SimpleOperation.operation.AsyncRun() +let ``Should be able to execute a query without sending input field asynchronously`` () : Task = task { + let! result = SimpleOperation.operation.AsyncRun (context) result |> SimpleOperation.validateResult None } [] -let ``Should be able to execute a query using context, without sending input field, asynchronously``() : Task = task { - let! result = SimpleOperation.operation.AsyncRun(context) +let ``Should be able to execute a query using context, without sending input field, asynchronously`` () : Task = task { + let! result = SimpleOperation.operation.AsyncRun (context) result |> SimpleOperation.validateResult None } [] -let ``Should be able to execute a query sending an empty input field``() = - let input = Input() - SimpleOperation.operation.Run(input) +let ``Should be able to execute a query sending an empty input field`` () = + let input = Input () + SimpleOperation.operation.Run (context, input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query using context, sending an empty input field``() = - let input = Input() - SimpleOperation.operation.Run(context, input) +let ``Should be able to execute a query using context, sending an empty input field`` () = + let input = Input () + SimpleOperation.operation.Run (context, input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query without sending an empty input field asynchronously``() : Task = task { - let input = Input() - let! result = SimpleOperation.operation.AsyncRun(input) +let ``Should be able to execute a query without sending an empty input field asynchronously`` () : Task = task { + let input = Input () + let! result = SimpleOperation.operation.AsyncRun (context, input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query using context, sending an empty input field, asynchronously``() : Task = task { - let input = Input() - let! result = SimpleOperation.operation.AsyncRun(context, input) +let ``Should be able to execute a query using context, sending an empty input field, asynchronously`` () : Task = task { + let input = Input () + let! result = SimpleOperation.operation.AsyncRun (context, input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query sending an input field with single field``() = - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let input = Input(single) - SimpleOperation.operation.Run(input) +let ``Should be able to execute a query sending an input field with single field`` () = + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let input = Input (single) + SimpleOperation.operation.Run (context, input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query using context, sending an input field with single field``() = - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let input = Input(single) - SimpleOperation.operation.Run(context, input) +let ``Should be able to execute a query using context, sending an input field with single field`` () = + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let input = Input (single) + SimpleOperation.operation.Run (context, input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query without sending an input field with single field asynchronously``() : Task = task { - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let input = Input(single) - let! result = SimpleOperation.operation.AsyncRun(input) +let ``Should be able to execute a query without sending an input field with single field asynchronously`` () : Task = task { + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let input = Input (single) + let! result = SimpleOperation.operation.AsyncRun (context, input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query using context, sending an input field with single field, asynchronously``() : Task = task { - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let input = Input(single) - let! result = SimpleOperation.operation.AsyncRun(context, input) +let ``Should be able to execute a query using context, sending an input field with single field, asynchronously`` () : Task = task { + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let input = Input (single) + let! result = SimpleOperation.operation.AsyncRun (context, input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query sending an input field with list field``() = - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(list) - SimpleOperation.operation.Run(input) +let ``Should be able to execute a query sending an input field with list field`` () = + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (list) + SimpleOperation.operation.Run (context, input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query using context, sending an input field with list field``() = - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(list) - SimpleOperation.operation.Run(context, input) +let ``Should be able to execute a query using context, sending an input field with list field`` () = + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (list) + SimpleOperation.operation.Run (context, input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query without sending an input field with list field asynchronously``() : Task = task { - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(list) - let! result = SimpleOperation.operation.AsyncRun(input) +let ``Should be able to execute a query without sending an input field with list field asynchronously`` () : Task = task { + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (list) + let! result = SimpleOperation.operation.AsyncRun (context, input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query using context, sending an input field with list field, asynchronously``() : Task = task { - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(list) - let! result = SimpleOperation.operation.AsyncRun(context, input) +let ``Should be able to execute a query using context, sending an input field with list field, asynchronously`` () : Task = task { + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (list) + let! result = SimpleOperation.operation.AsyncRun (context, input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query sending an input field with single and list fields``() = - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(single, list) - SimpleOperation.operation.Run(input) +let ``Should be able to execute a query sending an input field with single and list fields`` () = + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (single, list) + SimpleOperation.operation.Run (context, input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query using context, sending an input field with single and list fields``() = - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(single, list) - SimpleOperation.operation.Run(context, input) +let ``Should be able to execute a query using context, sending an input field with single and list fields`` () = + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (single, list) + SimpleOperation.operation.Run (context, input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query without sending an input field with single and list fields asynchronously``() : Task = task { - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(single, list) - let! result = SimpleOperation.operation.AsyncRun(input) +let ``Should be able to execute a query without sending an input field with single and list fields asynchronously`` () : Task = task { + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (single, list) + let! result = SimpleOperation.operation.AsyncRun (context, input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query using context, sending an input field with single and list fields, asynchronously``() : Task = task { - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(single, list) - let! result = SimpleOperation.operation.AsyncRun(context, input) +let ``Should be able to execute a query using context, sending an input field with single and list fields, asynchronously`` () : Task = task { + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (single, list) + let! result = SimpleOperation.operation.AsyncRun (context, input) result |> SimpleOperation.validateResult (Some input) } @@ -207,7 +221,7 @@ module SingleRequiredUploadOperation = contentType contentAsText } - }""">() + }"""> () type Operation = Provider.Operations.SingleUpload @@ -215,19 +229,29 @@ module SingleRequiredUploadOperation = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true result.Data.Value.SingleUpload.Name |> equals file.Name - result.Data.Value.SingleUpload.ContentAsText |> equals file.Content - result.Data.Value.SingleUpload.ContentType |> equals file.ContentType + result.Data.Value.SingleUpload.ContentAsText + |> equals file.Content + result.Data.Value.SingleUpload.ContentType + |> equals file.ContentType [] -let ``Should be able to execute a single required upload``() = - let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } - SingleRequiredUploadOperation.operation.Run(file.MakeUpload()) +let ``Should be able to execute a single required upload`` () = + let file = { + Name = "file.txt" + ContentType = "text/plain" + Content = "Sample text file contents" + } + SingleRequiredUploadOperation.operation.Run (context, file.MakeUpload ()) |> SingleRequiredUploadOperation.validateResult file [] -let ``Should be able to execute a single required upload asynchronously``() : Task = task { - let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } - let! result = SingleRequiredUploadOperation.operation.AsyncRun(file.MakeUpload()) +let ``Should be able to execute a single required upload asynchronously`` () : Task = task { + let file = { + Name = "file.txt" + ContentType = "text/plain" + Content = "Sample text file contents" + } + let! result = SingleRequiredUploadOperation.operation.AsyncRun (context, file.MakeUpload ()) result |> SingleRequiredUploadOperation.validateResult file } @@ -239,41 +263,54 @@ module SingleOptionalUploadOperation = contentType contentAsText } - }""">() + }"""> () type Operation = Provider.Operations.NullableSingleUpload let validateResult (file : File option) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true - file |> Option.iter (fun file -> - result.Data.Value.NullableSingleUpload.IsSome |> equals true - result.Data.Value.NullableSingleUpload.Value.Name |> equals file.Name - result.Data.Value.NullableSingleUpload.Value.ContentAsText |> equals file.Content - result.Data.Value.NullableSingleUpload.Value.ContentType |> equals file.ContentType) + file + |> Option.iter (fun file -> + result.Data.Value.NullableSingleUpload.IsSome |> equals true + result.Data.Value.NullableSingleUpload.Value.Name + |> equals file.Name + result.Data.Value.NullableSingleUpload.Value.ContentAsText + |> equals file.Content + result.Data.Value.NullableSingleUpload.Value.ContentType + |> equals file.ContentType) [] -let ``Should be able to execute a single optional upload by passing a file``() = - let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } - SingleOptionalUploadOperation.operation.Run(file.MakeUpload()) +let ``Should be able to execute a single optional upload by passing a file`` () = + let file = { + Name = "file.txt" + ContentType = "text/plain" + Content = "Sample text file contents" + } + SingleOptionalUploadOperation.operation.Run (context, file.MakeUpload ()) |> SingleOptionalUploadOperation.validateResult (Some file) -[] -let ``Should be able to execute a single optional upload by passing a file, asynchronously``() : Task = task { - let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } - let! result = SingleOptionalUploadOperation.operation.AsyncRun(file.MakeUpload()) - result |> SingleOptionalUploadOperation.validateResult (Some file) +[] +let ``Should be able to execute a single optional upload by passing a file, asynchronously`` () : Task = task { + let file = { + Name = "file.txt" + ContentType = "text/plain" + Content = "Sample text file contents" + } + let! result = SingleOptionalUploadOperation.operation.AsyncRun (context, file.MakeUpload ()) + result + |> SingleOptionalUploadOperation.validateResult (Some file) } -[] -let ``Should be able to execute a single optional upload by not passing a file``() = - SingleOptionalUploadOperation.operation.Run() +[] +let ``Should be able to execute a single optional upload by not passing a file`` () = + SingleOptionalUploadOperation.operation.Run (context) |> SingleOptionalUploadOperation.validateResult None [] -let ``Should be able to execute a single optional upload by not passing a file asynchronously``() : Task = task { - let! result = SingleOptionalUploadOperation.operation.AsyncRun() +let ``Should be able to execute a single optional upload by not passing a file asynchronously`` () : Task = task { + let! result = SingleOptionalUploadOperation.operation.AsyncRun (context) result |> SingleOptionalUploadOperation.validateResult None } @@ -285,33 +322,56 @@ module RequiredMultipleUploadOperation = contentType contentAsText } - }""">() + }"""> () type Operation = Provider.Operations.MultipleUpload - let validateResult (files : File []) (result : Operation.OperationResult) = + let validateResult (files : File[]) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true let receivedFiles = result.Data.Value.MultipleUpload - |> Array.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText }) + |> Array.map (fun file -> { + Name = file.Name + ContentType = file.ContentType + Content = file.ContentAsText + }) receivedFiles |> equals files [] -let ``Should be able to execute a multiple required upload``() = - let files = - [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - RequiredMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload())) +let ``Should be able to execute a multiple required upload`` () = + let files = [| + { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + RequiredMultipleUploadOperation.operation.Run (context, files |> Array.map (fun f -> f.MakeUpload ())) |> RequiredMultipleUploadOperation.validateResult files [] -let ``Should be able to execute a multiple required upload asynchronously``() : Task = task { - let files = - [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - let! result = RequiredMultipleUploadOperation.operation.AsyncRun(files |> Array.map (fun f -> f.MakeUpload())) - result |> RequiredMultipleUploadOperation.validateResult files +let ``Should be able to execute a multiple required upload asynchronously`` () : Task = task { + let files = [| + { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + let! result = RequiredMultipleUploadOperation.operation.AsyncRun (context, files |> Array.map (fun f -> f.MakeUpload ())) + result + |> RequiredMultipleUploadOperation.validateResult files } module OptionalMultipleUploadOperation = @@ -322,44 +382,70 @@ module OptionalMultipleUploadOperation = contentType contentAsText } - }""">() + }"""> () type Operation = Provider.Operations.NullableMultipleUpload - let validateResult (files : File [] option) (result : Operation.OperationResult) = + let validateResult (files : File[] option) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true let receivedFiles = result.Data.Value.NullableMultipleUpload - |> Option.map (Array.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText })) + |> Option.map ( + Array.map (fun file -> { + Name = file.Name + ContentType = file.ContentType + Content = file.ContentAsText + }) + ) receivedFiles |> equals files [] -let ``Should be able to execute a multiple upload``() = - let files = - [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - OptionalMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload())) +let ``Should be able to execute a multiple upload`` () = + let files = [| + { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + OptionalMultipleUploadOperation.operation.Run (context, files |> Array.map (fun f -> f.MakeUpload ())) |> OptionalMultipleUploadOperation.validateResult (Some files) [] -let ``Should be able to execute a multiple upload asynchronously``() : Task = task { - let files = - [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - let! result = OptionalMultipleUploadOperation.operation.AsyncRun(files |> Array.map (fun f -> f.MakeUpload())) - result |> OptionalMultipleUploadOperation.validateResult (Some files) +let ``Should be able to execute a multiple upload asynchronously`` () : Task = task { + let files = [| + { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + let! result = OptionalMultipleUploadOperation.operation.AsyncRun (context, files |> Array.map (fun f -> f.MakeUpload ())) + result + |> OptionalMultipleUploadOperation.validateResult (Some files) } [] -let ``Should be able to execute a multiple upload by sending no uploads``() = - OptionalMultipleUploadOperation.operation.Run() +let ``Should be able to execute a multiple upload by sending no uploads`` () = + OptionalMultipleUploadOperation.operation.Run (context) |> OptionalMultipleUploadOperation.validateResult None [] -let ``Should be able to execute a multiple upload asynchronously by sending no uploads``() : Task = task { - let! result = OptionalMultipleUploadOperation.operation.AsyncRun() - result |> OptionalMultipleUploadOperation.validateResult None +let ``Should be able to execute a multiple upload asynchronously by sending no uploads`` () : Task = task { + let! result = OptionalMultipleUploadOperation.operation.AsyncRun (context) + result + |> OptionalMultipleUploadOperation.validateResult None } module OptionalMultipleOptionalUploadOperation = @@ -370,65 +456,112 @@ module OptionalMultipleOptionalUploadOperation = contentType contentAsText } - }""">() + }"""> () type Operation = Provider.Operations.NullableMultipleNullableUpload - let validateResult (files : File option [] option) (result : Operation.OperationResult) = + let validateResult (files : File option[] option) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true let receivedFiles = result.Data.Value.NullableMultipleNullableUpload - |> Option.map (Array.map (Option.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText }))) + |> Option.map ( + Array.map ( + Option.map (fun file -> { + Name = file.Name + ContentType = file.ContentType + Content = file.ContentAsText + }) + ) + ) receivedFiles |> equals files [] -let ``Should be able to execute a multiple optional upload``() = - let files = - [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - OptionalMultipleOptionalUploadOperation.operation.Run(files |> Array.map (Option.map (fun f -> f.MakeUpload()))) +let ``Should be able to execute a multiple optional upload`` () = + let files = [| + Some { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + Some { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + OptionalMultipleOptionalUploadOperation.operation.Run (context, files |> Array.map (Option.map (fun f -> f.MakeUpload ()))) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) [] -let ``Should be able to execute a multiple optional upload asynchronously``() : Task = task { - let files = - [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(files |> Array.map (Option.map (fun f -> f.MakeUpload()))) - result |> (OptionalMultipleOptionalUploadOperation.validateResult (Some files)) +let ``Should be able to execute a multiple optional upload asynchronously`` () : Task = task { + let files = [| + Some { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + Some { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun (context, files |> Array.map (Option.map (fun f -> f.MakeUpload ()))) + result + |> (OptionalMultipleOptionalUploadOperation.validateResult (Some files)) } [] -let ``Should be able to execute a multiple optional upload by sending no uploads``() = - OptionalMultipleOptionalUploadOperation.operation.Run() +let ``Should be able to execute a multiple optional upload by sending no uploads`` () = + OptionalMultipleOptionalUploadOperation.operation.Run (context) |> OptionalMultipleOptionalUploadOperation.validateResult None [] -let ``Should be able to execute a multiple optional upload asynchronously by sending no uploads``() : Task = task { - let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun() - result |> OptionalMultipleOptionalUploadOperation.validateResult None +let ``Should be able to execute a multiple optional upload asynchronously by sending no uploads`` () : Task = task { + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun (context) + result + |> OptionalMultipleOptionalUploadOperation.validateResult None } [] -let ``Should be able to execute a multiple optional upload by sending some uploads``() = - let files = - [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - None - Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } - None |] - OptionalMultipleOptionalUploadOperation.operation.Run(files |> Array.map (Option.map (fun f -> f.MakeUpload()))) +let ``Should be able to execute a multiple optional upload by sending some uploads`` () = + let files = [| + Some { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + None + Some { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + None + |] + OptionalMultipleOptionalUploadOperation.operation.Run (context, files |> Array.map (Option.map (fun f -> f.MakeUpload ()))) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) [] -let ``Should be able to execute a multiple optional upload asynchronously by sending some uploads``() : Task = task { - let files = - [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - None - Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } - None |] - let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(files |> Array.map (Option.map (fun f -> f.MakeUpload()))) - result |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) +let ``Should be able to execute a multiple optional upload asynchronously by sending some uploads`` () : Task = task { + let files = [| + Some { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + None + Some { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + None + |] + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun (context, files |> Array.map (Option.map (fun f -> f.MakeUpload ()))) + result + |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) } module UploadRequestOperation = @@ -454,7 +587,7 @@ module UploadRequestOperation = name contentType contentAsText - }""">() + }"""> () type Operation = Provider.Operations.UploadRequestOperation @@ -463,28 +596,66 @@ module UploadRequestOperation = let validateResult (request : FilesRequest) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true - result.Data.Value.UploadRequest.Single.ToDictionary() |> File.FromDictionary |> equals request.Single - result.Data.Value.UploadRequest.Multiple |> Array.map ((fun x -> x.ToDictionary()) >> File.FromDictionary) |> equals request.Multiple - result.Data.Value.UploadRequest.NullableMultiple |> Option.map (Array.map ((fun x -> x.ToDictionary()) >> File.FromDictionary)) |> equals request.NullableMultiple - result.Data.Value.UploadRequest.NullableMultipleNullable |> Option.map (Array.map (Option.map ((fun x -> x.ToDictionary()) >> File.FromDictionary))) |> equals request.NullableMultipleNullable + result.Data.Value.UploadRequest.Single.ToDictionary () + |> File.FromDictionary + |> equals request.Single + result.Data.Value.UploadRequest.Multiple + |> Array.map ((fun x -> x.ToDictionary ()) >> File.FromDictionary) + |> equals request.Multiple + result.Data.Value.UploadRequest.NullableMultiple + |> Option.map (Array.map ((fun x -> x.ToDictionary ()) >> File.FromDictionary)) + |> equals request.NullableMultiple + result.Data.Value.UploadRequest.NullableMultipleNullable + |> Option.map (Array.map (Option.map ((fun x -> x.ToDictionary ()) >> File.FromDictionary))) + |> equals request.NullableMultipleNullable [] -let ``Should be able to upload files inside another input type``() : Task = task { - let request = - { Single = { Name = "single.txt"; ContentType = "text/plain"; Content = "Single file content" } - Multiple = - [| { Name = "multiple1.txt"; ContentType = "text/plain"; Content = "Multiple files first file content" } - { Name = "multiple2.txt"; ContentType = "text/plain"; Content = "Multiple files second file content" } |] - NullableMultiple = Some [| { Name = "multiple3.txt"; ContentType = "text/plain"; Content = "Multiple files third file content" } |] - NullableMultipleNullable = - Some [| Some { Name = "multiple4.txt"; ContentType = "text/plain"; Content = "Multiple files fourth file content" }; None |] } +let ``Should be able to upload files inside another input type`` () : Task = task { + let request = { + Single = { + Name = "single.txt" + ContentType = "text/plain" + Content = "Single file content" + } + Multiple = [| + { + Name = "multiple1.txt" + ContentType = "text/plain" + Content = "Multiple files first file content" + } + { + Name = "multiple2.txt" + ContentType = "text/plain" + Content = "Multiple files second file content" + } + |] + NullableMultiple = + Some [| + { + Name = "multiple3.txt" + ContentType = "text/plain" + Content = "Multiple files third file content" + } + |] + NullableMultipleNullable = + Some [| + Some { + Name = "multiple4.txt" + ContentType = "text/plain" + Content = "Multiple files fourth file content" + } + None + |] + } let input = - let makeUpload (x : File) = x.MakeUpload() - UploadRequestOperation.Request(single = makeUpload request.Single, - multiple = Array.map makeUpload request.Multiple, - nullableMultiple = Array.map makeUpload request.NullableMultiple.Value, - nullableMultipleNullable = Array.map (Option.map makeUpload) request.NullableMultipleNullable.Value) - let! result = UploadRequestOperation.operation.AsyncRun(input) + let makeUpload (x : File) = x.MakeUpload () + UploadRequestOperation.Request ( + single = makeUpload request.Single, + multiple = Array.map makeUpload request.Multiple, + nullableMultiple = Array.map makeUpload request.NullableMultiple.Value, + nullableMultipleNullable = Array.map (Option.map makeUpload) request.NullableMultipleNullable.Value + ) + let! result = UploadRequestOperation.operation.AsyncRun (context, input) result |> UploadRequestOperation.validateResult request } @@ -492,7 +663,7 @@ module UploadComplexOperation = let operation = Provider.Operation<"""mutation UploadComplex($input: InputFile!) { uploadComplex(input: $input) - }""">() + }"""> () type Operation = Provider.Operations.UploadComplex type InputFile = Provider.Types.InputFile @@ -504,30 +675,46 @@ module UploadComplexOperation = [] let ``Should be able to upload file using complex input object`` () = - let file = { Name = "complex.txt"; ContentType = "text/plain"; Content = "Complex input object file content" } - let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - UploadComplexOperation.operation.Run(input) + let file = { + Name = "complex.txt" + ContentType = "text/plain" + Content = "Complex input object file content" + } + let input = UploadComplexOperation.InputFile (file = file.MakeUpload ()) + UploadComplexOperation.operation.Run (context, input) |> UploadComplexOperation.validateResult file [] let ``Should be able to upload file using complex input object with context`` () = - let file = { Name = "complex_context.txt"; ContentType = "text/plain"; Content = "Complex input with context file content" } - let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - UploadComplexOperation.operation.Run(context, input) + let file = { + Name = "complex_context.txt" + ContentType = "text/plain" + Content = "Complex input with context file content" + } + let input = UploadComplexOperation.InputFile (file = file.MakeUpload ()) + UploadComplexOperation.operation.Run (context, input) |> UploadComplexOperation.validateResult file [] let ``Should be able to upload file using complex input object asynchronously`` () : Task = task { - let file = { Name = "complex_async.txt"; ContentType = "text/plain"; Content = "Complex input object async file content" } - let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - let! result = UploadComplexOperation.operation.AsyncRun(input) + let file = { + Name = "complex_async.txt" + ContentType = "text/plain" + Content = "Complex input object async file content" + } + let input = UploadComplexOperation.InputFile (file = file.MakeUpload ()) + let! result = UploadComplexOperation.operation.AsyncRun (context, input) result |> UploadComplexOperation.validateResult file } [] let ``Should be able to upload file using complex input object with context asynchronously`` () : Task = task { - let file = { Name = "complex_context_async.txt"; ContentType = "text/plain"; Content = "Complex input with context async file content" } - let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - let! result = UploadComplexOperation.operation.AsyncRun(context, input) + let file = { + Name = "complex_context_async.txt" + ContentType = "text/plain" + Content = "Complex input with context async file content" + } + let input = UploadComplexOperation.InputFile (file = file.MakeUpload ()) + let! result = UploadComplexOperation.operation.AsyncRun (context, input) result |> UploadComplexOperation.validateResult file } diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs index 74cc97a48..b0a0221ac 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs @@ -5,12 +5,16 @@ open System.Threading.Tasks open FSharp.Data.GraphQL open Helpers -let [] ServerUrl = "http://localhost:8085" -let [] EmptyGuidAsString = "00000000-0000-0000-0000-000000000000" +[] +let IntrospectionPath = "integration-introspection.json" +[] +let EmptyGuidAsString = "00000000-0000-0000-0000-000000000000" -type Provider = GraphQLProvider +type Provider = GraphQLProvider -let context = Provider.GetContext(ServerUrl) +let connection = TestHosts.createIntegrationConnection () +let context = + Provider.GetContext (serverUrl = TestHosts.integrationServerUrl, connectionFactory = (fun () -> connection)) type Input = Provider.Types.Input type InputField = Provider.Types.InputField @@ -36,165 +40,175 @@ module SimpleOperation = uri deprecated guid - }""">() + }"""> () type Operation = Provider.Operations.Q let validateResult (input : Input option) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Classic" result.Data.IsSome |> equals true - input |> Option.iter (fun input -> + input + |> Option.iter (fun input -> result.Data.Value.Echo.IsSome |> equals true - input.List |> Option.iter (fun list -> + input.List + |> Option.iter (fun list -> result.Data.Value.Echo.Value.List.IsSome |> equals true - let input = list |> Array.map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid.ToString()) - let output = result.Data.Value.Echo.Value.List.Value |> Array.map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid) + let input = + list + |> Array.map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid.ToString ()) + let output = + result.Data.Value.Echo.Value.List.Value + |> Array.map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid) input |> equals output) - input.Single |> Option.iter (fun single -> + input.Single + |> Option.iter (fun single -> result.Data.Value.Echo.Value.Single.IsSome |> equals true - let input = single.Int, single.IntOption, single.String, single.StringOption, single.Uri, single.Guid.ToString() - let output = result.Data.Value.Echo.Value.Single.Value |> map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid) + let input = + single.Int, single.IntOption, single.String, single.StringOption, single.Uri, single.Guid.ToString () + let output = + result.Data.Value.Echo.Value.Single.Value + |> map (fun x -> x.Int, x.IntOption, x.String, x.StringOption, x.Uri, x.Guid) input |> equals output)) [] -let ``Should be able to execute a query without sending input field``() = - SimpleOperation.operation.Run() +let ``Should be able to execute a query without sending input field`` () = + SimpleOperation.operation.Run (context) |> SimpleOperation.validateResult None [] -let ``Should be able to execute a query using context, without sending input field``() = - SimpleOperation.operation.Run(context) +let ``Should be able to execute a query using context, without sending input field`` () = + SimpleOperation.operation.Run (context) |> SimpleOperation.validateResult None [] -let ``Should be able to execute a query without sending input field asynchronously``() = - SimpleOperation.operation.AsyncRun() +let ``Should be able to execute a query without sending input field asynchronously`` () = + SimpleOperation.operation.AsyncRun (context) |> Async.RunSynchronously |> SimpleOperation.validateResult None [] -let ``Should be able to execute a query using context, without sending input field, asynchronously``() : Task = task { - let! result = SimpleOperation.operation.AsyncRun(context) +let ``Should be able to execute a query using context, without sending input field, asynchronously`` () : Task = task { + let! result = SimpleOperation.operation.AsyncRun (context) result |> SimpleOperation.validateResult None } [] -let ``Should be able to execute a query sending an empty input field``() = - let input = Input() - SimpleOperation.operation.Run(Some input) +let ``Should be able to execute a query sending an empty input field`` () = + let input = Input () + SimpleOperation.operation.Run (context, Some input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query using context, sending an empty input field``() = - let input = Input() - SimpleOperation.operation.Run(context, Some input) +let ``Should be able to execute a query using context, sending an empty input field`` () = + let input = Input () + SimpleOperation.operation.Run (context, Some input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query without sending an empty input field asynchronously``() : Task = task { - let input = Input() - let! result = SimpleOperation.operation.AsyncRun(Some input) +let ``Should be able to execute a query without sending an empty input field asynchronously`` () : Task = task { + let input = Input () + let! result = SimpleOperation.operation.AsyncRun (context, Some input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query using context, sending an empty input field, asynchronously``() : Task = task { - let input = Input() - let! result = SimpleOperation.operation.AsyncRun(context, Some input) +let ``Should be able to execute a query using context, sending an empty input field, asynchronously`` () : Task = task { + let input = Input () + let! result = SimpleOperation.operation.AsyncRun (context, Some input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query sending an input field with single field``() = - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let input = Input(Some single) - SimpleOperation.operation.Run(Some input) +let ``Should be able to execute a query sending an input field with single field`` () = + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let input = Input (Some single) + SimpleOperation.operation.Run (context, Some input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query using context, sending an input field with single field``() = - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let input = Input(Some single) - SimpleOperation.operation.Run(context, Some input) +let ``Should be able to execute a query using context, sending an input field with single field`` () = + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let input = Input (Some single) + SimpleOperation.operation.Run (context, Some input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query without sending an input field with single field asynchronously``() : Task = task { - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let input = Input(Some single) - let! result = SimpleOperation.operation.AsyncRun(Some input) +let ``Should be able to execute a query without sending an input field with single field asynchronously`` () : Task = task { + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let input = Input (Some single) + let! result = SimpleOperation.operation.AsyncRun (context, Some input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query using context, sending an input field with single field, asynchronously``() : Task = task { - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let input = Input(Some single) - let! result = SimpleOperation.operation.AsyncRun(context, Some input) +let ``Should be able to execute a query using context, sending an input field with single field, asynchronously`` () : Task = task { + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let input = Input (Some single) + let! result = SimpleOperation.operation.AsyncRun (context, Some input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query sending an input field with list field``() = - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(list = Some list) - SimpleOperation.operation.Run(Some input) +let ``Should be able to execute a query sending an input field with list field`` () = + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (list = Some list) + SimpleOperation.operation.Run (context, Some input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query using context, sending an input field with list field``() = - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(list = Some list) - SimpleOperation.operation.Run(context, Some input) +let ``Should be able to execute a query using context, sending an input field with list field`` () = + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (list = Some list) + SimpleOperation.operation.Run (context, Some input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query without sending an input field with list field asynchronously``() : Task = task { - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(list = Some list) - let! result = SimpleOperation.operation.AsyncRun(Some input) +let ``Should be able to execute a query without sending an input field with list field asynchronously`` () : Task = task { + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (list = Some list) + let! result = SimpleOperation.operation.AsyncRun (context, Some input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query using context, sending an input field with list field, asynchronously``() : Task = task { - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(list = Some list) - let! result = SimpleOperation.operation.AsyncRun(context, Some input) +let ``Should be able to execute a query using context, sending an input field with list field, asynchronously`` () : Task = task { + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (list = Some list) + let! result = SimpleOperation.operation.AsyncRun (context, Some input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query sending an input field with single and list fields``() = - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(Some single, Some list) - SimpleOperation.operation.Run(Some input) +let ``Should be able to execute a query sending an input field with single and list fields`` () = + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (Some single, Some list) + SimpleOperation.operation.Run (context, Some input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query using context, sending an input field with single and list fields``() = - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(Some single, Some list) - SimpleOperation.operation.Run(context, Some input) +let ``Should be able to execute a query using context, sending an input field with single and list fields`` () = + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (Some single, Some list) + SimpleOperation.operation.Run (context, Some input) |> SimpleOperation.validateResult (Some input) [] -let ``Should be able to execute a query without sending an input field with single and list fields asynchronously``() : Task = task { - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(Some single, Some list) - let! result = SimpleOperation.operation.AsyncRun(Some input) +let ``Should be able to execute a query without sending an input field with single and list fields asynchronously`` () : Task = task { + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (Some single, Some list) + let! result = SimpleOperation.operation.AsyncRun (context, Some input) result |> SimpleOperation.validateResult (Some input) } [] -let ``Should be able to execute a query using context, sending an input field with single and list fields, asynchronously``() : Task = task { - let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) - let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] - let input = Input(Some single, Some list) - let! result = SimpleOperation.operation.AsyncRun(context, Some input) +let ``Should be able to execute a query using context, sending an input field with single and list fields, asynchronously`` () : Task = task { + let single = InputField ("A", 2, System.Uri ("http://localhost:1234"), EmptyGuidAsString) + let list = [| InputField ("A", 2, System.Uri ("http://localhost:4321"), EmptyGuidAsString) |] + let input = Input (Some single, Some list) + let! result = SimpleOperation.operation.AsyncRun (context, Some input) result |> SimpleOperation.validateResult (Some input) } @@ -206,7 +220,7 @@ module SingleRequiredUploadOperation = contentType contentAsText } - }""">() + }"""> () type Operation = Provider.Operations.SingleUpload @@ -214,19 +228,29 @@ module SingleRequiredUploadOperation = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true result.Data.Value.SingleUpload.Name |> equals file.Name - result.Data.Value.SingleUpload.ContentAsText |> equals file.Content - result.Data.Value.SingleUpload.ContentType |> equals file.ContentType + result.Data.Value.SingleUpload.ContentAsText + |> equals file.Content + result.Data.Value.SingleUpload.ContentType + |> equals file.ContentType [] -let ``Should be able to execute a single required upload``() = - let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } - SingleRequiredUploadOperation.operation.Run(file.MakeUpload(file.Name)) +let ``Should be able to execute a single required upload`` () = + let file = { + Name = "file.txt" + ContentType = "text/plain" + Content = "Sample text file contents" + } + SingleRequiredUploadOperation.operation.Run (context, file.MakeUpload (file.Name)) |> SingleRequiredUploadOperation.validateResult file [] -let ``Should be able to execute a single required upload asynchronously``() : Task = task { - let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } - let! result = SingleRequiredUploadOperation.operation.AsyncRun(file.MakeUpload()) +let ``Should be able to execute a single required upload asynchronously`` () : Task = task { + let file = { + Name = "file.txt" + ContentType = "text/plain" + Content = "Sample text file contents" + } + let! result = SingleRequiredUploadOperation.operation.AsyncRun (context, file.MakeUpload ()) result |> SingleRequiredUploadOperation.validateResult file } @@ -238,40 +262,53 @@ module SingleOptionalUploadOperation = contentType contentAsText } - }""">() + }"""> () type Operation = Provider.Operations.NullableSingleUpload let validateResult (file : File option) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true - file |> Option.iter (fun file -> - result.Data.Value.NullableSingleUpload.IsSome |> equals true - result.Data.Value.NullableSingleUpload.Value.Name |> equals file.Name - result.Data.Value.NullableSingleUpload.Value.ContentAsText |> equals file.Content - result.Data.Value.NullableSingleUpload.Value.ContentType |> equals file.ContentType) + file + |> Option.iter (fun file -> + result.Data.Value.NullableSingleUpload.IsSome |> equals true + result.Data.Value.NullableSingleUpload.Value.Name + |> equals file.Name + result.Data.Value.NullableSingleUpload.Value.ContentAsText + |> equals file.Content + result.Data.Value.NullableSingleUpload.Value.ContentType + |> equals file.ContentType) [] -let ``Should be able to execute a single optional upload by passing a file``() = - let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } - SingleOptionalUploadOperation.operation.Run(file.MakeUpload() |> Some) +let ``Should be able to execute a single optional upload by passing a file`` () = + let file = { + Name = "file.txt" + ContentType = "text/plain" + Content = "Sample text file contents" + } + SingleOptionalUploadOperation.operation.Run (context, file.MakeUpload () |> Some) |> SingleOptionalUploadOperation.validateResult (Some file) [] -let ``Should be able to execute a single optional upload by passing a file, asynchronously``() : Task = task { - let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } - let! result = SingleOptionalUploadOperation.operation.AsyncRun(file.MakeUpload("test") |> Some) - result |> SingleOptionalUploadOperation.validateResult (Some file) +let ``Should be able to execute a single optional upload by passing a file, asynchronously`` () : Task = task { + let file = { + Name = "file.txt" + ContentType = "text/plain" + Content = "Sample text file contents" + } + let! result = SingleOptionalUploadOperation.operation.AsyncRun (context, file.MakeUpload ("test") |> Some) + result + |> SingleOptionalUploadOperation.validateResult (Some file) } [] -let ``Should be able to execute a single optional upload by not passing a file``() = - SingleOptionalUploadOperation.operation.Run() +let ``Should be able to execute a single optional upload by not passing a file`` () = + SingleOptionalUploadOperation.operation.Run (context) |> SingleOptionalUploadOperation.validateResult None [] -let ``Should be able to execute a single optional upload by not passing a file asynchronously``() : Task = task { - let! result = SingleOptionalUploadOperation.operation.AsyncRun() +let ``Should be able to execute a single optional upload by not passing a file asynchronously`` () : Task = task { + let! result = SingleOptionalUploadOperation.operation.AsyncRun (context) result |> SingleOptionalUploadOperation.validateResult None } @@ -283,33 +320,56 @@ module RequiredMultipleUploadOperation = contentType contentAsText } - }""">() + }"""> () type Operation = Provider.Operations.MultipleUpload - let validateResult (files : File []) (result : Operation.OperationResult) = + let validateResult (files : File[]) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true let receivedFiles = result.Data.Value.MultipleUpload - |> Array.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText }) + |> Array.map (fun file -> { + Name = file.Name + ContentType = file.ContentType + Content = file.ContentAsText + }) receivedFiles |> equals files [] -let ``Should be able to execute a multiple required upload``() = - let files = - [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - RequiredMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload())) +let ``Should be able to execute a multiple required upload`` () = + let files = [| + { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + RequiredMultipleUploadOperation.operation.Run (context, files |> Array.map (fun f -> f.MakeUpload ())) |> RequiredMultipleUploadOperation.validateResult files [] -let ``Should be able to execute a multiple required upload asynchronously``() : Task = task { - let files = - [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - let! result = RequiredMultipleUploadOperation.operation.AsyncRun(files |> Array.map (fun f -> f.MakeUpload())) - result |> RequiredMultipleUploadOperation.validateResult files +let ``Should be able to execute a multiple required upload asynchronously`` () : Task = task { + let files = [| + { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + let! result = RequiredMultipleUploadOperation.operation.AsyncRun (context, files |> Array.map (fun f -> f.MakeUpload ())) + result + |> RequiredMultipleUploadOperation.validateResult files } module OptionalMultipleUploadOperation = @@ -320,44 +380,70 @@ module OptionalMultipleUploadOperation = contentType contentAsText } - }""">() + }"""> () type Operation = Provider.Operations.NullableMultipleUpload - let validateResult (files : File [] option) (result : Operation.OperationResult) = + let validateResult (files : File[] option) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true let receivedFiles = result.Data.Value.NullableMultipleUpload - |> Option.map (Array.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText })) + |> Option.map ( + Array.map (fun file -> { + Name = file.Name + ContentType = file.ContentType + Content = file.ContentAsText + }) + ) receivedFiles |> equals files [] -let ``Should be able to execute a multiple upload``() = - let files = - [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - OptionalMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload()) |> Some) +let ``Should be able to execute a multiple upload`` () = + let files = [| + { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + OptionalMultipleUploadOperation.operation.Run (context, files |> Array.map (fun f -> f.MakeUpload ()) |> Some) |> OptionalMultipleUploadOperation.validateResult (Some files) [] -let ``Should be able to execute a multiple upload asynchronously``() : Task = task { - let files = - [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - let! result = OptionalMultipleUploadOperation.operation.AsyncRun((files |> Array.map _.MakeUpload()) |> Some) - result |> OptionalMultipleUploadOperation.validateResult (Some files) +let ``Should be able to execute a multiple upload asynchronously`` () : Task = task { + let files = [| + { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + let! result = OptionalMultipleUploadOperation.operation.AsyncRun (context, (files |> Array.map _.MakeUpload()) |> Some) + result + |> OptionalMultipleUploadOperation.validateResult (Some files) } [] -let ``Should be able to execute a multiple upload by sending no uploads``() = - OptionalMultipleUploadOperation.operation.Run() +let ``Should be able to execute a multiple upload by sending no uploads`` () = + OptionalMultipleUploadOperation.operation.Run (context) |> OptionalMultipleUploadOperation.validateResult None [] -let ``Should be able to execute a multiple upload asynchronously by sending no uploads``() : Task = task { - let! result = OptionalMultipleUploadOperation.operation.AsyncRun() - result |> OptionalMultipleUploadOperation.validateResult None +let ``Should be able to execute a multiple upload asynchronously by sending no uploads`` () : Task = task { + let! result = OptionalMultipleUploadOperation.operation.AsyncRun (context) + result + |> OptionalMultipleUploadOperation.validateResult None } module OptionalMultipleOptionalUploadOperation = @@ -368,65 +454,112 @@ module OptionalMultipleOptionalUploadOperation = contentType contentAsText } - }""">() + }"""> () type Operation = Provider.Operations.NullableMultipleNullableUpload - let validateResult (files : File option [] option) (result : Operation.OperationResult) = + let validateResult (files : File option[] option) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true let receivedFiles = result.Data.Value.NullableMultipleNullableUpload - |> Option.map (Array.map (Option.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText }))) + |> Option.map ( + Array.map ( + Option.map (fun file -> { + Name = file.Name + ContentType = file.ContentType + Content = file.ContentAsText + }) + ) + ) receivedFiles |> equals files [] -let ``Should be able to execute a multiple optional upload``() = - let files = - [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - OptionalMultipleOptionalUploadOperation.operation.Run((files |> Array.map (Option.map _.MakeUpload())) |> Some) +let ``Should be able to execute a multiple optional upload`` () = + let files = [| + Some { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + Some { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + OptionalMultipleOptionalUploadOperation.operation.Run (context, (files |> Array.map (Option.map _.MakeUpload())) |> Some) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) [] -let ``Should be able to execute a multiple optional upload asynchronously``() : Task = task { - let files = - [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun((files |> Array.map (Option.map _.MakeUpload())) |> Some) - result |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) +let ``Should be able to execute a multiple optional upload asynchronously`` () : Task = task { + let files = [| + Some { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + Some { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + |] + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun (context, (files |> Array.map (Option.map _.MakeUpload())) |> Some) + result + |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) } [] -let ``Should be able to execute a multiple optional upload by sending no uploads``() = - OptionalMultipleOptionalUploadOperation.operation.Run() +let ``Should be able to execute a multiple optional upload by sending no uploads`` () = + OptionalMultipleOptionalUploadOperation.operation.Run (context) |> OptionalMultipleOptionalUploadOperation.validateResult None [] -let ``Should be able to execute a multiple optional upload asynchronously by sending no uploads``() : Task = task { - let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun() - result |> OptionalMultipleOptionalUploadOperation.validateResult None +let ``Should be able to execute a multiple optional upload asynchronously by sending no uploads`` () : Task = task { + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun (context) + result + |> OptionalMultipleOptionalUploadOperation.validateResult None } [] -let ``Should be able to execute a multiple optional upload by sending some uploads``() = - let files = - [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - None - Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } - None |] - OptionalMultipleOptionalUploadOperation.operation.Run(files |> Array.map (Option.map _.MakeUpload()) |> Some) +let ``Should be able to execute a multiple optional upload by sending some uploads`` () = + let files = [| + Some { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + None + Some { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + None + |] + OptionalMultipleOptionalUploadOperation.operation.Run (context, files |> Array.map (Option.map _.MakeUpload()) |> Some) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) [] -let ``Should be able to execute a multiple optional upload asynchronously by sending some uploads``() : Task = task { - let files = - [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } - None - Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } - None |] - let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(files |> Array.map (Option.map _.MakeUpload()) |> Some) - result |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) +let ``Should be able to execute a multiple optional upload asynchronously by sending some uploads`` () : Task = task { + let files = [| + Some { + Name = "file1.txt" + ContentType = "text/plain" + Content = "Sample text file contents 1" + } + None + Some { + Name = "file2.txt" + ContentType = "text/plain" + Content = "Sample text file contents 2" + } + None + |] + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun (context, files |> Array.map (Option.map _.MakeUpload()) |> Some) + result + |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) } module UploadRequestOperation = @@ -452,7 +585,7 @@ module UploadRequestOperation = name contentType contentAsText - }""">() + }"""> () type Operation = Provider.Operations.UploadRequestOperation @@ -461,35 +594,73 @@ module UploadRequestOperation = let validateResult (request : FilesRequest) (result : Operation.OperationResult) = result |> checkRequestTypeHeader "Multipart" result.Data.IsSome |> equals true - result.Data.Value.UploadRequest.Single.ToDictionary() |> File.FromDictionary |> equals request.Single - result.Data.Value.UploadRequest.Multiple |> Array.map ((fun x -> x.ToDictionary()) >> File.FromDictionary) |> equals request.Multiple - result.Data.Value.UploadRequest.NullableMultiple |> Option.map (Array.map ((fun x -> x.ToDictionary()) >> File.FromDictionary)) |> equals request.NullableMultiple - result.Data.Value.UploadRequest.NullableMultipleNullable |> Option.map (Array.map (Option.map ((fun x -> x.ToDictionary()) >> File.FromDictionary))) |> equals request.NullableMultipleNullable + result.Data.Value.UploadRequest.Single.ToDictionary () + |> File.FromDictionary + |> equals request.Single + result.Data.Value.UploadRequest.Multiple + |> Array.map ((fun x -> x.ToDictionary ()) >> File.FromDictionary) + |> equals request.Multiple + result.Data.Value.UploadRequest.NullableMultiple + |> Option.map (Array.map ((fun x -> x.ToDictionary ()) >> File.FromDictionary)) + |> equals request.NullableMultiple + result.Data.Value.UploadRequest.NullableMultipleNullable + |> Option.map (Array.map (Option.map ((fun x -> x.ToDictionary ()) >> File.FromDictionary))) + |> equals request.NullableMultipleNullable [] -let ``Should be able to upload files inside another input type``() = - let request = - { Single = { Name = "single.txt"; ContentType = "text/plain"; Content = "Single file content" } - Multiple = - [| { Name = "multiple1.txt"; ContentType = "text/plain"; Content = "Multiple files first file content" } - { Name = "multiple2.txt"; ContentType = "text/plain"; Content = "Multiple files second file content" } |] - NullableMultiple = Some [| { Name = "multiple3.txt"; ContentType = "text/plain"; Content = "Multiple files third file content" } |] - NullableMultipleNullable = - Some [| Some { Name = "multiple4.txt"; ContentType = "text/plain"; Content = "Multiple files fourth file content" }; None |] } +let ``Should be able to upload files inside another input type`` () = + let request = { + Single = { + Name = "single.txt" + ContentType = "text/plain" + Content = "Single file content" + } + Multiple = [| + { + Name = "multiple1.txt" + ContentType = "text/plain" + Content = "Multiple files first file content" + } + { + Name = "multiple2.txt" + ContentType = "text/plain" + Content = "Multiple files second file content" + } + |] + NullableMultiple = + Some [| + { + Name = "multiple3.txt" + ContentType = "text/plain" + Content = "Multiple files third file content" + } + |] + NullableMultipleNullable = + Some [| + Some { + Name = "multiple4.txt" + ContentType = "text/plain" + Content = "Multiple files fourth file content" + } + None + |] + } let input = - let makeUpload (x : File) = x.MakeUpload() - UploadRequestOperation.Request(single = makeUpload request.Single, - multiple = Array.map makeUpload request.Multiple, - nullableMultiple = Some (Array.map makeUpload request.NullableMultiple.Value), - nullableMultipleNullable = Some (Array.map (Option.map makeUpload) request.NullableMultipleNullable.Value)) - UploadRequestOperation.operation.Run(input) + let makeUpload (x : File) = x.MakeUpload () + UploadRequestOperation.Request ( + single = makeUpload request.Single, + multiple = Array.map makeUpload request.Multiple, + nullableMultiple = Some (Array.map makeUpload request.NullableMultiple.Value), + nullableMultipleNullable = Some (Array.map (Option.map makeUpload) request.NullableMultipleNullable.Value) + ) + UploadRequestOperation.operation.Run (context, input) |> UploadRequestOperation.validateResult request module UploadComplexOperation = let operation = Provider.Operation<"""mutation UploadComplex($input: InputFile!) { uploadComplex(input: $input) - }""">() + }"""> () type Operation = Provider.Operations.UploadComplex type InputFile = Provider.Types.InputFile @@ -501,30 +672,46 @@ module UploadComplexOperation = [] let ``Should be able to upload file using complex input object`` () = - let file = { Name = "complex.txt"; ContentType = "text/plain"; Content = "Complex input object file content" } - let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - UploadComplexOperation.operation.Run(input) + let file = { + Name = "complex.txt" + ContentType = "text/plain" + Content = "Complex input object file content" + } + let input = UploadComplexOperation.InputFile (file = file.MakeUpload ()) + UploadComplexOperation.operation.Run (context, input) |> UploadComplexOperation.validateResult file [] let ``Should be able to upload file using complex input object with context`` () = - let file = { Name = "complex_context.txt"; ContentType = "text/plain"; Content = "Complex input with context file content" } - let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - UploadComplexOperation.operation.Run(context, input) + let file = { + Name = "complex_context.txt" + ContentType = "text/plain" + Content = "Complex input with context file content" + } + let input = UploadComplexOperation.InputFile (file = file.MakeUpload ()) + UploadComplexOperation.operation.Run (context, input) |> UploadComplexOperation.validateResult file [] let ``Should be able to upload file using complex input object asynchronously`` () : Task = task { - let file = { Name = "complex_async.txt"; ContentType = "text/plain"; Content = "Complex input object async file content" } - let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - let! result = UploadComplexOperation.operation.AsyncRun(input) + let file = { + Name = "complex_async.txt" + ContentType = "text/plain" + Content = "Complex input object async file content" + } + let input = UploadComplexOperation.InputFile (file = file.MakeUpload ()) + let! result = UploadComplexOperation.operation.AsyncRun (context, input) result |> UploadComplexOperation.validateResult file } [] let ``Should be able to upload file using complex input object with context asynchronously`` () : Task = task { - let file = { Name = "complex_context_async.txt"; ContentType = "text/plain"; Content = "Complex input with context async file content" } - let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - let! result = UploadComplexOperation.operation.AsyncRun(context, input) + let file = { + Name = "complex_context_async.txt" + ContentType = "text/plain" + Content = "Complex input with context async file content" + } + let input = UploadComplexOperation.InputFile (file = file.MakeUpload ()) + let! result = UploadComplexOperation.operation.AsyncRun (context, input) result |> UploadComplexOperation.validateResult file } diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs index f95b44df0..5080dc195 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs @@ -7,9 +7,13 @@ open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Client [] -let ServerUrl = "http://localhost:8085" +let IntrospectionPath = "integration-introspection.json" -type Provider = GraphQLProvider +type Provider = GraphQLProvider + +let connection = TestHosts.createIntegrationConnection () +let context = + Provider.GetContext (serverUrl = TestHosts.integrationServerUrl, connectionFactory = (fun () -> connection)) module ErrorOperation = let operation = @@ -106,7 +110,7 @@ let ``Should parse all combinations of optional operation error fields`` () = [] let ``Should map server error extensions and locations into operation result`` () = - let result = ErrorOperation.operation.Run () + let result = ErrorOperation.operation.Run (context) result.Errors.Length |> equals 1 diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/ReservedScalarNameProviderTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/ReservedScalarNameProviderTests.fs index 3a938226b..0d3df7de9 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/ReservedScalarNameProviderTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/ReservedScalarNameProviderTests.fs @@ -15,10 +15,10 @@ module ObjectDateSchema = value category } - }""">() + }"""> () let compileSmoke () = - let schemaDate = SchemaDate(value = "2026-04-03", category = "default") + let schemaDate = SchemaDate (value = "2026-04-03", category = "default") let operationInstance : ObjectDateProvider.Operations.Q = operation schemaDate |> ignore operationInstance |> ignore @@ -29,19 +29,17 @@ module InputDateSchema = let operation = InputDateProvider.Operation<"""query Q($input: Date) { echoDate(input: $input) - }""">() + }"""> () let compileSmoke () = - let schemaDate = SchemaDate(value = "2026-04-03", category = "default") + let schemaDate = SchemaDate (value = "2026-04-03", category = "default") let deferredRun : unit -> _ = - fun () -> operation.Run(Unchecked.defaultof, schemaDate) + fun () -> operation.Run (Unchecked.defaultof, schemaDate) schemaDate |> ignore deferredRun |> ignore [] -let ``Should allow object types that reuse reserved scalar names`` () = - ObjectDateSchema.compileSmoke () +let ``Should allow object types that reuse reserved scalar names`` () = ObjectDateSchema.compileSmoke () [] -let ``Should allow input object types that reuse reserved scalar names`` () = - InputDateSchema.compileSmoke () +let ``Should allow input object types that reuse reserved scalar names`` () = InputDateSchema.compileSmoke () diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiLocalProviderTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiLocalProviderTests.fs index e673cba63..892f1636c 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiLocalProviderTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiLocalProviderTests.fs @@ -3,17 +3,17 @@ module FSharp.Data.GraphQL.IntegrationTests.SwapiLocalProviderTests open Xunit open Helpers open FSharp.Data.GraphQL -open System.Net.Http open System.Threading.Tasks // Local provider should be able to be created from local introspection json file. type Provider = GraphQLProvider<"introspection.json"> // We are going to re-use the same HttpClient through all requests. -let connection = new GraphQLClientConnection(new HttpClient()) +let connection = TestHosts.createStarWarsConnection () // As we are not using a connection to a server to get the introspection, we need a runtime context. -let getContext() = Provider.GetContext(serverUrl = "http://localhost:8086", connectionFactory = fun () -> connection) +let getContext () = + Provider.GetContext (serverUrl = TestHosts.starWarsServerUrl, connectionFactory = (fun () -> connection)) type Episode = Provider.Types.Episode @@ -45,26 +45,33 @@ hero (id: "1000") { } } } - }""">() + }"""> () type Operation = Provider.Operations.Q let validateResult (result : Operation.OperationResult) = - result.CustomData.ContainsKey("documentId") |> equals true + result.CustomData.ContainsKey ("documentId") |> equals true result.Errors |> equals [||] result.Data.IsSome |> equals true result.Data.Value.Hero.IsSome |> equals true - result.Data.Value.Hero.Value.AppearsIn |> equals [| Episode.NewHope; Episode.Empire; Episode.Jedi |] - let expectedFriends : Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Character array = - [| Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Han Solo") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Leia Organa", homePlanet = "Alderaan") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "C-3PO", primaryFunction = "Protocol") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "R2-D2", primaryFunction = "Astromech") |] - let friends = result.Data.Value.Hero.Value.Friends.Edges |> Array.map (fun x -> x.Node) + result.Data.Value.Hero.Value.AppearsIn + |> equals [| Episode.NewHope; Episode.Empire; Episode.Jedi |] + let expectedFriends : Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Character array = [| + Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Han Solo") + Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Leia Organa", homePlanet = "Alderaan") + Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "C-3PO", primaryFunction = "Protocol") + Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "R2-D2", primaryFunction = "Astromech") + |] + let friends = + result.Data.Value.Hero.Value.Friends.Edges + |> Array.map (fun x -> x.Node) friends |> equals expectedFriends - result.Data.Value.Hero.Value.HomePlanet |> equals (Some "Tatooine") + result.Data.Value.Hero.Value.HomePlanet + |> equals (Some "Tatooine") let actual = normalize <| sprintf "%A" result.Data - let expected = normalize <| """Some + let expected = + normalize + <| """Some {Hero = Some {AppearsIn = [|NewHope; Empire; Jedi|]; Friends = {Edges = [|{Cursor = "RnJpZW5kOjEwMDI="; @@ -88,54 +95,62 @@ hero (id: "1000") { [] let ``Should be able to start a simple query operation synchronously`` () = - use context = getContext() - SimpleOperation.operation.Run(context) + use context = getContext () + SimpleOperation.operation.Run (context) |> SimpleOperation.validateResult [] let ``Should be able to start a simple query operation asynchronously`` () : Task = task { - use context = getContext() - let! result = SimpleOperation.operation.AsyncRun(context) + use context = getContext () + let! result = SimpleOperation.operation.AsyncRun (context) result |> SimpleOperation.validateResult } [] let ``Should be able to use pattern matching methods on an union type`` () = - use context = getContext() - let result = SimpleOperation.operation.Run(context) + use context = getContext () + let result = SimpleOperation.operation.Run (context) result.Data.IsSome |> equals true result.Data.Value.Hero.IsSome |> equals true - let friends = result.Data.Value.Hero.Value.Friends.Edges |> Array.map (fun x -> x.Node) + let friends = + result.Data.Value.Hero.Value.Friends.Edges + |> Array.map (fun x -> x.Node) friends - |> Array.choose (fun x -> x.TryAsHuman()) + |> Array.choose (fun x -> x.TryAsHuman ()) |> equals [| - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Han Solo") - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Leia Organa", homePlanet = "Alderaan") |] + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Han Solo") + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Leia Organa", homePlanet = "Alderaan") + |] friends - |> Array.choose (fun x -> x.TryAsDroid()) + |> Array.choose (fun x -> x.TryAsDroid ()) |> equals [| - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "C-3PO", primaryFunction = "Protocol") - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "R2-D2", primaryFunction = "Astromech") |] + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "C-3PO", primaryFunction = "Protocol") + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "R2-D2", primaryFunction = "Astromech") + |] try - friends |> Array.map (fun x -> x.AsDroid()) |> ignore - failwith "Expected exception when trying to get all friends as droids!" - with _ -> () + friends |> Array.map (fun x -> x.AsDroid ()) |> ignore + failwith "Expected exception when trying to get all friends as droids!" + with _ -> + () try - friends |> Array.map (fun x -> x.AsHuman()) |> ignore - failwith "Expected exception when trying to get all friends as humans!" - with _ -> () + friends |> Array.map (fun x -> x.AsHuman ()) |> ignore + failwith "Expected exception when trying to get all friends as humans!" + with _ -> + () friends - |> Array.filter (fun x -> x.IsHuman()) - |> Array.map (fun x -> x.AsHuman()) + |> Array.filter (fun x -> x.IsHuman ()) + |> Array.map (fun x -> x.AsHuman ()) |> equals [| - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Han Solo") - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Leia Organa", homePlanet = "Alderaan") |] + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Han Solo") + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Leia Organa", homePlanet = "Alderaan") + |] friends - |> Array.filter (fun x -> x.IsDroid()) - |> Array.map (fun x -> x.AsDroid()) + |> Array.filter (fun x -> x.IsDroid ()) + |> Array.map (fun x -> x.AsDroid ()) |> equals [| - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "C-3PO", primaryFunction = "Protocol") - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "R2-D2", primaryFunction = "Astromech") |] + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "C-3PO", primaryFunction = "Protocol") + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "R2-D2", primaryFunction = "Astromech") + |] module MutationOperation = let operation = @@ -145,52 +160,60 @@ module MutationOperation = name isMoon } - }""">() + }"""> () type Operation = Provider.Operations.M let validateResult (result : Operation.OperationResult) = - result.CustomData.ContainsKey("documentId") |> equals true + result.CustomData.ContainsKey ("documentId") |> equals true result.Errors |> equals [||] result.Data.IsSome |> equals true result.Data.Value.SetMoon.IsSome |> equals true result.Data.Value.SetMoon.Value.Id |> equals "1" - result.Data.Value.SetMoon.Value.Name |> equals (Some "Tatooine") + result.Data.Value.SetMoon.Value.Name + |> equals (Some "Tatooine") result.Data.Value.SetMoon.Value.IsMoon |> equals (Some true) [] let ``Should be able to run a mutation synchronously`` () = - use context = getContext() - MutationOperation.operation.Run(context) + use context = getContext () + MutationOperation.operation.Run (context) |> MutationOperation.validateResult [] let ``Should be able to run a mutation asynchronously`` () : Task = task { - use context = getContext() - let! result = MutationOperation.operation.AsyncRun(context) + use context = getContext () + let! result = MutationOperation.operation.AsyncRun (context) result |> MutationOperation.validateResult } module FileOperation = - let fileop = Provider.Operation<"operation.graphql">() + let fileop = Provider.Operation<"operation.graphql"> () type Operation = Provider.Operations.FileOp let validateResult (result : Operation.OperationResult) = - result.CustomData.ContainsKey("documentId") |> equals true + result.CustomData.ContainsKey ("documentId") |> equals true result.Errors |> equals [||] result.Data.IsSome |> equals true result.Data.Value.Hero.IsSome |> equals true - result.Data.Value.Hero.Value.AppearsIn |> equals [| Episode.NewHope; Episode.Empire; Episode.Jedi |] - let expectedFriends : Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Character array = - [| Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Han Solo") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Leia Organa", homePlanet = "Alderaan") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "C-3PO", primaryFunction = "Protocol") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "R2-D2", primaryFunction = "Astromech") |] - let friends = result.Data.Value.Hero.Value.Friends.Edges |> Array.map (fun x -> x.Node) + result.Data.Value.Hero.Value.AppearsIn + |> equals [| Episode.NewHope; Episode.Empire; Episode.Jedi |] + let expectedFriends : Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Character array = [| + Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Han Solo") + Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Leia Organa", homePlanet = "Alderaan") + Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "C-3PO", primaryFunction = "Protocol") + Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "R2-D2", primaryFunction = "Astromech") + |] + let friends = + result.Data.Value.Hero.Value.Friends.Edges + |> Array.map (fun x -> x.Node) friends |> equals expectedFriends - result.Data.Value.Hero.Value.HomePlanet |> equals (Some "Tatooine") + result.Data.Value.Hero.Value.HomePlanet + |> equals (Some "Tatooine") let actual = normalize <| sprintf "%A" result.Data - let expected = normalize <| """Some + let expected = + normalize + <| """Some {Hero = Some {AppearsIn = [|NewHope; Empire; Jedi|]; Friends = {Edges = [|{Cursor = "RnJpZW5kOjEwMDI="; @@ -214,6 +237,6 @@ module FileOperation = [] let ``Should be able to run a query from a query file`` () = - use context = getContext() - FileOperation.fileop.Run(context) + use context = getContext () + FileOperation.fileop.Run (context) |> FileOperation.validateResult diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs index dc73b38db..bc92cb8a7 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs @@ -5,7 +5,11 @@ open Helpers open FSharp.Data.GraphQL open System.Threading.Tasks -type Provider = GraphQLProvider<"http://localhost:8086"> +type Provider = GraphQLProvider<"introspection.json"> + +let connection = TestHosts.createStarWarsConnection () +let context = + Provider.GetContext (serverUrl = TestHosts.starWarsServerUrl, connectionFactory = (fun () -> connection)) type Episode = Provider.Types.Episode @@ -37,26 +41,48 @@ hero (id: "1000") { } } } - }""">() + }"""> () type Operation = Provider.Operations.Q let validateResult (result : Operation.OperationResult) = - result.CustomData.ContainsKey("documentId") |> equals true + result.CustomData.ContainsKey ("documentId") |> equals true result.Errors |> equals [||] result.Data.IsSome |> equals true result.Data.Value.Hero.IsSome |> equals true - result.Data.Value.Hero.Value.AppearsIn |> equals [| Episode.NewHope; Episode.Empire; Episode.Jedi |] - let expectedFriends : Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Character array = - [| Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Han Solo") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Leia Organa", homePlanet = "Alderaan") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "C-3PO", primaryFunction = "Protocol") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "R2-D2", primaryFunction = "Astromech") |] - let friends = result.Data.Value.Hero.Value.Friends.Edges |> Array.map (fun e -> e.Node) - friends |> equals expectedFriends - result.Data.Value.Hero.Value.HomePlanet |> equals (Some "Tatooine") + result.Data.Value.Hero.Value.AppearsIn + |> equals [| Episode.NewHope; Episode.Empire; Episode.Jedi |] + let friends = + result.Data.Value.Hero.Value.Friends.Edges + |> Array.map (fun e -> e.Node) + friends.Length |> equals 4 + do + let friend0 = friends[0] + friend0.IsHuman () |> equals true + friend0.AsHuman().Name |> equals (Some "Han Solo") + do + let friend1 = friends[1] + friend1.IsHuman () |> equals true + friend1.AsHuman().Name |> equals (Some "Leia Organa") + friend1.AsHuman().HomePlanet |> equals (Some "Alderaan") + do + let friend2 = friends[2] + friend2.IsDroid () |> equals true + friend2.AsDroid().Name |> equals (Some "C-3PO") + friend2.AsDroid().PrimaryFunction + |> equals (Some "Protocol") + do + let friend3 = friends[3] + friend3.IsDroid () |> equals true + friend3.AsDroid().Name |> equals (Some "R2-D2") + friend3.AsDroid().PrimaryFunction + |> equals (Some "Astromech") + result.Data.Value.Hero.Value.HomePlanet + |> equals (Some "Tatooine") let actual = normalize <| sprintf "%A" result.Data - let expected = normalize <| """Some + let expected = + normalize + <| """Some {Hero = Some {AppearsIn = [|NewHope; Empire; Jedi|]; Friends = {Edges = [|{Cursor = "RnJpZW5kOjEwMDI="; @@ -80,51 +106,59 @@ hero (id: "1000") { [] let ``Should be able to start a simple query operation synchronously`` () = - SimpleOperation.operation.Run() + SimpleOperation.operation.Run (context) |> SimpleOperation.validateResult [] let ``Should be able to start a simple query operation asynchronously`` () : Task = task { - let! result = SimpleOperation.operation.AsyncRun() + let! result = SimpleOperation.operation.AsyncRun (context) result |> SimpleOperation.validateResult } [] let ``Should be able to use pattern matching methods on an union type`` () = - let result = SimpleOperation.operation.Run() + let result = SimpleOperation.operation.Run (context) result.Data.IsSome |> equals true result.Data.Value.Hero.IsSome |> equals true - let friends = result.Data.Value.Hero.Value.Friends.Edges |> Array.map (fun e -> e.Node) + let friends = + result.Data.Value.Hero.Value.Friends.Edges + |> Array.map (fun e -> e.Node) friends - |> Array.choose (fun x -> x.TryAsHuman()) + |> Array.choose (fun x -> x.TryAsHuman ()) |> equals [| - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Han Solo") - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Leia Organa", homePlanet = "Alderaan") |] + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Han Solo") + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Leia Organa", homePlanet = "Alderaan") + |] friends - |> Array.choose (fun x -> x.TryAsDroid()) + |> Array.choose (fun x -> x.TryAsDroid ()) |> equals [| - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "C-3PO", primaryFunction = "Protocol") - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "R2-D2", primaryFunction = "Astromech") |] + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "C-3PO", primaryFunction = "Protocol") + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "R2-D2", primaryFunction = "Astromech") + |] try - friends |> Array.map (fun x -> x.AsDroid()) |> ignore - failwith "Expected exception when trying to get all friends as droids!" - with _ -> () + friends |> Array.map (fun x -> x.AsDroid ()) |> ignore + failwith "Expected exception when trying to get all friends as droids!" + with _ -> + () try - friends |> Array.map (fun x -> x.AsHuman()) |> ignore - failwith "Expected exception when trying to get all friends as humans!" - with _ -> () + friends |> Array.map (fun x -> x.AsHuman ()) |> ignore + failwith "Expected exception when trying to get all friends as humans!" + with _ -> + () friends - |> Array.filter (fun x -> x.IsHuman()) - |> Array.map (fun x -> x.AsHuman()) + |> Array.filter (fun x -> x.IsHuman ()) + |> Array.map (fun x -> x.AsHuman ()) |> equals [| - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Han Solo") - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Leia Organa", homePlanet = "Alderaan") |] + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Han Solo") + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human (name = "Leia Organa", homePlanet = "Alderaan") + |] friends - |> Array.filter (fun x -> x.IsDroid()) - |> Array.map (fun x -> x.AsDroid()) + |> Array.filter (fun x -> x.IsDroid ()) + |> Array.map (fun x -> x.AsDroid ()) |> equals [| - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "C-3PO", primaryFunction = "Protocol") - SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "R2-D2", primaryFunction = "Astromech") |] + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "C-3PO", primaryFunction = "Protocol") + SimpleOperation.Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid (name = "R2-D2", primaryFunction = "Astromech") + |] module MutationOperation = let operation = @@ -134,51 +168,74 @@ module MutationOperation = name isMoon } - }""">() + }"""> () type Operation = Provider.Operations.M let validateResult (result : Operation.OperationResult) = - result.CustomData.ContainsKey("documentId") |> equals true + result.CustomData.ContainsKey ("documentId") |> equals true result.Errors |> equals [||] result.Data.IsSome |> equals true result.Data.Value.SetMoon.IsSome |> equals true result.Data.Value.SetMoon.Value.Id |> equals "1" - result.Data.Value.SetMoon.Value.Name |> equals (Some "Tatooine") + result.Data.Value.SetMoon.Value.Name + |> equals (Some "Tatooine") result.Data.Value.SetMoon.Value.IsMoon |> equals (Some true) [] let ``Should be able to run a mutation synchronously`` () = - MutationOperation.operation.Run() + MutationOperation.operation.Run (context) |> MutationOperation.validateResult [] let ``Should be able to run a mutation asynchronously`` () : Task = task { - let! result = MutationOperation.operation.AsyncRun() + let! result = MutationOperation.operation.AsyncRun (context) result |> MutationOperation.validateResult } module FileOperation = - let fileOp = Provider.Operation<"operation.graphql">() + let fileOp = Provider.Operation<"operation.graphql"> () type Operation = Provider.Operations.FileOp let validateResult (result : Operation.OperationResult) = - result.CustomData.ContainsKey("documentId") |> equals true + result.CustomData.ContainsKey ("documentId") |> equals true result.Errors |> equals [||] result.Data.IsSome |> equals true result.Data.Value.Hero.IsSome |> equals true - result.Data.Value.Hero.Value.AppearsIn |> equals [| Episode.NewHope; Episode.Empire; Episode.Jedi |] - let expectedFriends : Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Character array = - [| Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Han Solo") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Leia Organa", homePlanet = "Alderaan") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "C-3PO", primaryFunction = "Protocol") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "R2-D2", primaryFunction = "Astromech") |] - let friends = result.Data.Value.Hero.Value.Friends.Edges |> Array.map _.Node - friends |> equals expectedFriends - result.Data.Value.Hero.Value.HomePlanet |> equals (Some "Tatooine") + result.Data.Value.Hero.Value.AppearsIn + |> equals [| Episode.NewHope; Episode.Empire; Episode.Jedi |] + let friends = + result.Data.Value.Hero.Value.Friends.Edges + |> Array.map _.Node + friends.Length |> equals 4 + do + let friend0 = friends[0] + friend0.IsHuman () |> equals true + friend0.AsHuman().Name |> equals (Some "Han Solo") + do + let friend1 = friends[1] + friend1.IsHuman () |> equals true + friend1.AsHuman().Name |> equals (Some "Leia Organa") + friend1.AsHuman().HomePlanet |> equals (Some "Alderaan") + do + let friend2 = friends[2] + friend2.IsDroid () |> equals true + friend2.AsDroid().Name |> equals (Some "C-3PO") + friend2.AsDroid().PrimaryFunction + |> equals (Some "Protocol") + do + let friend3 = friends[3] + friend3.IsDroid () |> equals true + friend3.AsDroid().Name |> equals (Some "R2-D2") + friend3.AsDroid().PrimaryFunction + |> equals (Some "Astromech") + result.Data.Value.Hero.Value.HomePlanet + |> equals (Some "Tatooine") let actual = normalize <| sprintf "%A" result.Data - let expected = normalize <| """Some + let expected = + normalize + <| """Some {Hero = Some {AppearsIn = [|NewHope; Empire; Jedi|]; Friends = {Edges = [|{Cursor = "RnJpZW5kOjEwMDI="; @@ -202,5 +259,5 @@ module FileOperation = [] let ``Should be able to run a query from a query file`` () = - FileOperation.fileOp.Run() + FileOperation.fileOp.Run (context) |> FileOperation.validateResult diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/TestHosts.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/TestHosts.fs new file mode 100644 index 000000000..c2faa8a00 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/TestHosts.fs @@ -0,0 +1,41 @@ +module FSharp.Data.GraphQL.IntegrationTests.TestHosts + +open FSharp.Data.GraphQL +open Microsoft.AspNetCore.Mvc.Testing +open System.Net.Http +open System + +type IntegrationServerApplicationFactory () = + inherit WebApplicationFactory () + +type StarWarsApplicationFactory () = + inherit WebApplicationFactory () + +let private integrationFactory = lazy (new IntegrationServerApplicationFactory ()) +let private starWarsFactory = lazy (new StarWarsApplicationFactory ()) + +let createIntegrationHttpClient () : HttpClient = integrationFactory.Value.CreateClient () + +let createStarWarsHttpClient () : HttpClient = starWarsFactory.Value.CreateClient () + +let private getIntegrationServerUrl () = + use client = createIntegrationHttpClient () + client.BaseAddress.ToString().TrimEnd '/' + +let private getStarWarsServerUrl () = + use client = createStarWarsHttpClient () + client.BaseAddress.ToString().TrimEnd '/' + +do + AppDomain.CurrentDomain.ProcessExit.Add (fun _ -> + if integrationFactory.IsValueCreated then + integrationFactory.Value.Dispose () + + if starWarsFactory.IsValueCreated then + starWarsFactory.Value.Dispose ()) + +let integrationServerUrl = getIntegrationServerUrl () +let starWarsServerUrl = getStarWarsServerUrl () + +let createIntegrationConnection () = new GraphQLClientConnection (createIntegrationHttpClient ()) +let createStarWarsConnection () = new GraphQLClientConnection (createStarWarsHttpClient ()) diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json b/tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json new file mode 100644 index 000000000..ad448c42f --- /dev/null +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json @@ -0,0 +1,1929 @@ +{ + "documentId": 986164407, + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": null, + "types": [ + { + "kind": "SCALAR", + "name": "Int", + "description": "The \u0060Int\u0060 scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The \u0060String\u0060 scalar type represents textual data, represented as UTF-8 character sequences. The \u0060String\u0060 type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The \u0060Boolean\u0060 scalar type represents \u0060true\u0060 or \u0060false\u0060.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Float", + "description": "The \u0060Float\u0060 scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The \u0060ID\u0060 scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The \u0060ID\u0060 type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as \u0060\u00224\u0022\u0060) or integer (such as \u00604\u0060) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "DateTimeOffset", + "description": "The \u0060DateTimeOffset\u0060 scalar type represents a Date value with Time component. The \u0060DateTimeOffset\u0060 type appears in a JSON response as a String representation compatible with ISO-8601 format.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "DateOnly", + "description": "The \u0060DateOnly\u0060 scalar type represents a Date value without Time component. The \u0060DateOnly\u0060 type appears in a JSON response as a \u0060String\u0060 representation of full-date value as specified by [IETF 3339](https://www.ietf.org/rfc/rfc3339.txt).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "TimeOnly", + "description": "The \u0060TimeOnly\u0060 scalar type represents a Time value without Date component. The \u0060TimeOnly\u0060 type appears in a JSON response as a \u0060String\u0060 representation of full-time value as specified by [IETF 3339](https://www.ietf.org/rfc/rfc3339.txt).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "URI", + "description": "The \u0060URI\u0060 scalar type represents a string resource identifier compatible with URI standard. The \u0060URI\u0060 type appears in a JSON response as a String.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. In some cases, you need to provide options to alter GraphQL\u2019s execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onField", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onFragment", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onOperation", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "defaultValue", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the \u0060__TypeKind\u0060 enum. Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given __Type is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. \u0060fields\u0060 and \u0060interfaces\u0060 are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. \u0060fields\u0060 and \u0060possibleTypes\u0060 are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. \u0060possibleTypes\u0060 is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. \u0060enumValues\u0060 is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. \u0060inputFields\u0060 is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. \u0060ofType\u0060 is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. \u0060ofType\u0060 is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to a field argument IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to an union IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field IDL definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Query", + "description": "The query type.", + "fields": [ + { + "name": "alwaysError", + "description": "Always produces an execution error for integration tests.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "echo", + "description": "Enters an input type and get it back.", + "args": [ + { + "name": "input", + "description": "The input to be echoed as an output.", + "type": { + "kind": "INPUT_OBJECT", + "name": "Input", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Output", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Output", + "description": "The output for an input.", + "fields": [ + { + "name": "list", + "description": "A list of output fields.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "OutputField", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "single", + "description": "A single output field.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutputField", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "OutputField", + "description": "The output for a field input.", + "fields": [ + { + "name": "deprecated", + "description": "A string value through a deprecated field.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "This field is deprecated." + }, + { + "name": "guid", + "description": "A Guid value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "guidId", + "description": "A Guid Id value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "guidIdOption", + "description": "A Guid Id value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "int", + "description": "An integer value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "intOption", + "description": "An integer option value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "string", + "description": "A string value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "stringId", + "description": "A String Id value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "stringIdOption", + "description": "A String Id value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "stringOption", + "description": "A string option value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uri", + "description": "An URI value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "URI", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Guid", + "description": "The \u0060Guid\u0060 scalar type represents a Globally Unique Identifier value. It\u0027s a 128-bit long byte key, that can be serialized to string.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "Input", + "description": "Input object type.", + "fields": null, + "inputFields": [ + { + "name": "single", + "description": "A single input field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "InputField", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "list", + "description": "A list of input fields.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InputField", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InputField", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "string", + "description": "A string value.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "int", + "description": "An integer value.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "stringOption", + "description": "A string option value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "intOption", + "description": "An integer option value.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "uri", + "description": "An URI value.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "URI", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "guid", + "description": "A Guid value.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": null, + "fields": [ + { + "name": "multipleUpload", + "description": "Uploads a list of files to the server and get them back.", + "args": [ + { + "name": "files", + "description": "The files to upload.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullableMultipleNullableUpload", + "description": "Uploads (maybe) a list of files (maybe) to the server and get them back (maybe).", + "args": [ + { + "name": "files", + "description": "The files to upload.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullableMultipleUpload", + "description": "Uploads (maybe) a list of files to the server and get them back (maybe).", + "args": [ + { + "name": "files", + "description": "The files to upload.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullableSingleUpload", + "description": "Uploads (maybe) a single file to the server and get it back (maybe).", + "args": [ + { + "name": "file", + "description": "The file to be uploaded.", + "type": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "singleUpload", + "description": "Uploads a single file to the server and get it back.", + "args": [ + { + "name": "file", + "description": "The file to be uploaded.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uploadComplex", + "description": "", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InputFile", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uploadRequest", + "description": "Upload several files in different forms.", + "args": [ + { + "name": "request", + "description": "The request for uploading several files in different forms.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UploadRequest", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UploadedFile", + "description": "Contains data of an uploaded file.", + "fields": [ + { + "name": "contentAsText", + "description": "The content of the file as text.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contentType", + "description": "The content type of the file.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the file.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "File", + "description": "The \u0060File\u0060 type represents a file on one or more fields of an object in an object list. The filter is represented by a JSON object where the fields are the complemented by specific suffixes to represent a query.", + "fields": null, + "inputFields": [], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InputFile", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "file", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UploadResponse", + "description": "Contains uploaded files of an upload files request.", + "fields": [ + { + "name": "multiple", + "description": "Multiple file uploads.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullableMultiple", + "description": "Optional list of multiple file uploads.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullableMultipleNullable", + "description": "Optional list of multiple optional file uploads.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "single", + "description": "A single file upload.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UploadRequest", + "description": "Request for uploading files in several different forms.", + "fields": null, + "inputFields": [ + { + "name": "single", + "description": "A single file upload.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "multiple", + "description": "Multiple file uploads.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + } + } + }, + "defaultValue": null + }, + { + "name": "nullableMultiple", + "description": "Optional list of multiple file uploads.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "nullableMultipleNullable", + "description": "Optional list of multiple optional file uploads.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the \u0060if\u0060 argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the \u0060if\u0060 argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "defer", + "description": "Defers the resolution of this field or fragment", + "locations": [ + "FIELD", + "FRAGMENT_DEFINITION", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [] + }, + { + "name": "stream", + "description": "Streams the resolution of this field or fragment", + "locations": [ + "FIELD", + "FRAGMENT_DEFINITION", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [] + }, + { + "name": "live", + "description": "Subscribes for live updates of this field or fragment", + "locations": [ + "FIELD", + "FRAGMENT_DEFINITION", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [] + } + ] + } + } +} \ No newline at end of file diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json index 7b3d9abcf..a961111fd 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json @@ -1,5 +1,5 @@ { - "documentId": -128167532, + "documentId": 195530235, "data": { "__schema": { "queryType": { From 1506a22528d916b044f3227faa7f1f906430a607 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 20:02:19 +0200 Subject: [PATCH 02/32] Enforce input/output kind safety for `ListOf`/`Nullable` wrappers at compile time (#569) Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Andrii Chebukin --- .../SchemaDefinitions.fs | 86 +++++++++-- .../SchemaDefinitionsExtensions.fs | 6 +- src/FSharp.Data.GraphQL.Shared/TypeSystem.fs | 39 +++-- .../FSharp.Data.GraphQL.Tests.fsproj | 10 ++ .../PlanningTests.fs | 8 +- .../TypeWrappersKindSafety/.gitignore | 1 + .../ListOf.InputAsOutput.fsx | 12 ++ .../ListOf.OutputAsInput.fsx | 12 ++ .../Nullable.InputAsOutput.fsx | 12 ++ .../Nullable.OutputAsInput.fsx | 12 ++ .../StructNullable.InputAsOutput.fsx | 12 ++ .../StructNullable.OutputAsInput.fsx | 12 ++ .../TypeWrappersKindSafety/Valid.fsx | 20 +++ .../TypeWrappersKindSafetyTests.fs | 143 ++++++++++++++++++ .../Variables and Inputs/InputComplexTests.fs | 2 +- .../Variables and Inputs/InputNestedTests.fs | 2 +- 16 files changed, 354 insertions(+), 35 deletions(-) create mode 100644 tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/.gitignore create mode 100644 tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.InputAsOutput.fsx create mode 100644 tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.OutputAsInput.fsx create mode 100644 tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.InputAsOutput.fsx create mode 100644 tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.OutputAsInput.fsx create mode 100644 tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.InputAsOutput.fsx create mode 100644 tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.OutputAsInput.fsx create mode 100644 tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Valid.fsx create mode 100644 tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafetyTests.fs diff --git a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs index 51f8682c5..bc33ab3e8 100644 --- a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs @@ -392,17 +392,73 @@ module SchemaDefinitions = | false, _ -> getParseError destinationType s | InlineConstant value -> value.GetCoerceError destinationType - /// Wraps a GraphQL type definition, allowing defining field/argument - /// to take option of provided value. - let Nullable(innerDef : #TypeDef<'Val>) : NullableDef<'Val> = upcast { NullableDefinition.OfType = innerDef } - - /// Wraps a GraphQL type definition, allowing defining field/argument - /// to take voption of provided value. - let StructNullable(innerDef : #TypeDef<'Val>) : StructNullableDef<'Val> = upcast { StructNullableDefinition.OfType = innerDef } - - /// Wraps a GraphQL type definition, allowing defining field/argument - /// to take collection of provided value. - let ListOf(innerDef : #TypeDef<'Val>) : ListOfDef<'Val, 'Seq> = upcast { ListOfDefinition.OfType = innerDef } + type TypeWrapperStaticDispatch = + + static member Nullable<'Val>(innerDef : InputOutputDef<'Val>) : NullableDef<'Val> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { NullableDefinition.OfType = ofType } + + static member Nullable<'Val>(innerDef : InputDef<'Val>) : InputDef<'Val option> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { NullableDefinition.OfType = ofType } + + static member Nullable<'Val>(innerDef : OutputDef<'Val>) : OutputDef<'Val option> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { NullableDefinition.OfType = ofType } + + static member StructNullable<'Val>(innerDef : InputOutputDef<'Val>) : StructNullableDef<'Val> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { StructNullableDefinition.OfType = ofType } + + static member StructNullable<'Val>(innerDef : InputDef<'Val>) : InputDef<'Val voption> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { StructNullableDefinition.OfType = ofType } + + static member StructNullable<'Val>(innerDef : OutputDef<'Val>) : OutputDef<'Val voption> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { StructNullableDefinition.OfType = ofType } + + static member ListOf<'Val, 'Seq when 'Seq :> 'Val seq>(innerDef : InputOutputDef<'Val>) : ListOfDef<'Val, 'Seq> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { ListOfDefinition.OfType = ofType } + + static member ListOf<'Val, 'Seq when 'Seq :> 'Val seq>(innerDef : InputDef<'Val>) : InputDef<'Seq> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { ListOfDefinition.OfType = ofType } + + static member ListOf<'Val, 'Seq when 'Seq :> 'Val seq>(innerDef : OutputDef<'Val>) : OutputDef<'Seq> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { ListOfDefinition.OfType = ofType } + + /// Wraps a GraphQL input or output type definition, allowing defining field/argument + /// to take option of provided value while preserving input/output kind of wrapped type. + /// Input wrappers produce input definitions, output wrappers produce output definitions, + /// and wrappers over types implementing both kinds keep both capabilities. + /// Dispatch is selected at compile time via SRTP. + let inline Nullable< ^Def, ^Wrapped when (^Def or TypeWrapperStaticDispatch) : (static member Nullable : ^Def -> ^Wrapped) > + (innerDef : ^Def) + : ^Wrapped = + ((^Def or TypeWrapperStaticDispatch) : (static member Nullable : ^Def -> ^Wrapped) innerDef) + + /// Wraps a GraphQL input or output type definition, allowing defining field/argument + /// to take voption of provided value while preserving input/output kind of wrapped type. + /// Input wrappers produce input definitions, output wrappers produce output definitions, + /// and wrappers over types implementing both kinds keep both capabilities. + /// Dispatch is selected at compile time via SRTP. + let inline StructNullable< ^Def, ^Wrapped when (^Def or TypeWrapperStaticDispatch) : (static member StructNullable : ^Def -> ^Wrapped) > + (innerDef : ^Def) + : ^Wrapped = + ((^Def or TypeWrapperStaticDispatch) : (static member StructNullable : ^Def -> ^Wrapped) innerDef) + + /// Wraps a GraphQL input or output type definition, allowing defining field/argument + /// to take collection of provided value while preserving input/output kind of wrapped type. + /// Input wrappers produce input definitions, output wrappers produce output definitions, + /// and wrappers over types implementing both kinds keep both capabilities. + /// Dispatch is selected at compile time via SRTP. + let inline ListOf< ^Def, ^Wrapped when (^Def or TypeWrapperStaticDispatch) : (static member ListOf : ^Def -> ^Wrapped) > + (innerDef : ^Def) + : ^Wrapped = + ((^Def or TypeWrapperStaticDispatch) : (static member ListOf : ^Def -> ^Wrapped) innerDef) let internal variableOrElse other (_ : InputExecutionContextProvider) value (variables : IReadOnlyDictionary) = match value with @@ -1415,13 +1471,14 @@ module SchemaDefinitions = /// If defined, this value will be used when no matching input has been provided by the requester. /// Optional input description. Usefull for generating documentation. static member SkippableInput(name : string, typedef : #InputDef<'In>, ?description : string) : InputFieldDef = + let typedef : InputDef<'In> = upcast typedef 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 + match (box typedef) with + | :? NullableDef<'In> as n -> (n :> InputDef<'In option>) + | _ -> Nullable typedef DefaultValue = None ExecuteInput = Unchecked.defaultof } @@ -1539,4 +1596,3 @@ module SchemaDefinitions = Description = description FieldsFn = fun () -> fieldsFn() |> List.toArray ResolveType = resolveType } - diff --git a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitionsExtensions.fs b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitionsExtensions.fs index b11909644..1da3341c7 100644 --- a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitionsExtensions.fs +++ b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitionsExtensions.fs @@ -27,8 +27,10 @@ type internal CustomFieldsObjectDefinition<'Val> (source : ObjectDef<'Val>, fiel member _.Implements = source.Implements member _.IsTypeOf = source.IsTypeOf interface TypeDef with - member this.MakeList () = upcast (ListOf this) - member this.MakeNullable () = upcast (Nullable this) + // We construct wrappers directly here because this API works with untyped TypeDef values. + // The public ListOf/Nullable helpers use SRTP dispatch and require statically known direction. + member this.MakeList () = upcast { ListOfDefinition.OfType = this } + member this.MakeNullable () = upcast { NullableDefinition.OfType = this } member _.Type = (source :> TypeDef).Type interface NamedDef with member _.Name = (source :> NamedDef).Name diff --git a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs index 11006387b..be8e79b55 100644 --- a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs +++ b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs @@ -609,6 +609,24 @@ and OutputDef<'Val> = inherit TypeDef<'Val> end +/// Representation of type definitions that can be used as both inputs and outputs +/// (for example scalars and enums). This marker is also used by SRTP wrapper dispatch. +and InputOutputDef = + interface + inherit InputDef + inherit OutputDef + end + +/// Representation of all type definitions, that can be used as both inputs and outputs +/// and are constrained to represent the provided .NET type. +and InputOutputDef<'Val> = + interface + inherit InputOutputDef + inherit TypeDef<'Val> + inherit InputDef<'Val> + inherit OutputDef<'Val> + end + /// Representation of leaf type definitions. Leaf types represents leafs /// of the GraphQL query tree. Each query path must end with a leaf. /// By default only scalars and enums are valid leaf types. @@ -1067,8 +1085,7 @@ and ScalarDef = abstract CoerceOutput : obj -> obj option inherit TypeDef inherit NamedDef - inherit InputDef - inherit OutputDef + inherit InputOutputDef inherit LeafDef end @@ -1098,6 +1115,7 @@ and [] ScalarDefinition<'Primitive, 'Val> = { interface InputDef interface OutputDef + interface InputOutputDef interface ScalarDef with member x.Name = x.Name @@ -1107,6 +1125,7 @@ and [] ScalarDefinition<'Primitive, 'Val> = { interface InputDef<'Val> interface OutputDef<'Val> + interface InputOutputDef<'Val> interface LeafDef interface NamedDef with @@ -1182,8 +1201,7 @@ and EnumDef = /// List of available enum cases. abstract Options : EnumVal[] inherit TypeDef - inherit InputDef - inherit OutputDef + inherit InputOutputDef inherit LeafDef inherit NamedDef end @@ -1197,8 +1215,7 @@ and EnumDef<'Val> = abstract Options : EnumValue<'Val>[] inherit EnumDef inherit TypeDef<'Val> - inherit InputDef<'Val> - inherit OutputDef<'Val> + inherit InputOutputDef<'Val> end and internal EnumDefinition<'Val> = { @@ -1212,6 +1229,7 @@ and internal EnumDefinition<'Val> = { interface InputDef interface OutputDef + interface InputOutputDef interface TypeDef with member _.Type = typeof<'Val> @@ -1523,8 +1541,7 @@ and ListOfDef<'Val, 'Seq when 'Seq :> 'Val seq> = /// GraphQL type definition of the container element type. abstract OfType : TypeDef<'Val> inherit TypeDef<'Seq> - inherit InputDef<'Seq> - inherit OutputDef<'Seq> + inherit InputOutputDef<'Seq> inherit ListOfDef end @@ -1574,8 +1591,7 @@ and NullableDef<'Val> = interface /// GraphQL type definition of the nested type. abstract OfType : TypeDef<'Val> - inherit InputDef<'Val option> - inherit OutputDef<'Val option> + inherit InputOutputDef<'Val option> inherit NullableDef end @@ -1614,8 +1630,7 @@ and StructNullableDef<'Val> = interface /// GraphQL type definition of the nested type. abstract OfType : TypeDef<'Val> - inherit InputDef<'Val voption> - inherit OutputDef<'Val voption> + inherit InputOutputDef<'Val voption> inherit NullableDef end 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 763d841ec..1bc224559 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -41,6 +41,16 @@ + + + + + + + + + + diff --git a/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs b/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs index 57b330da4..ad7cf74d5 100644 --- a/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs @@ -139,7 +139,7 @@ let ``Planning must retain correct types for lists``() = } } }""" - let PersonList : ListOfDef = ListOf Person + let PersonList : OutputDef = ListOf Person let plan = schemaProcessor.CreateExecutionPlanOrFail(query) equals 1 plan.Fields.Length let listInfo = plan.Fields.Head @@ -178,7 +178,7 @@ let ``Planning must work with interfaces``() = }""" let plan = schemaProcessor.CreateExecutionPlanOrFail(query) equals 1 plan.Fields.Length - let INamedList : ListOfDef = ListOf INamed + let INamedList : OutputDef = ListOf INamed let listInfo = plan.Fields.Head listInfo.Identifier |> equals "names" listInfo.ReturnDef |> equals (upcast INamedList) @@ -215,7 +215,7 @@ let ``Planning must work with unions``() = let plan = schemaProcessor.CreateExecutionPlanOrFail(query) equals 1 plan.Fields.Length let listInfo = plan.Fields.Head - let UNamedList : ListOfDef = ListOf UNamed + let UNamedList : OutputDef = ListOf UNamed listInfo.Identifier |> equals "names" listInfo.ReturnDef |> equals (upcast UNamedList) let (ResolveCollection(info)) = listInfo.Kind @@ -309,7 +309,7 @@ let ``Planning must handle inline fragment with non-matching type condition in u // Verify the execution plan structure equals 1 plan.Fields.Length let listInfo = plan.Fields.Head - let UNamedList : ListOfDef = ListOf UNamed + let UNamedList : OutputDef = ListOf UNamed listInfo.Identifier |> equals "names" listInfo.ReturnDef |> equals (upcast UNamedList) let (ResolveCollection(info)) = listInfo.Kind diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/.gitignore b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/.gitignore new file mode 100644 index 000000000..a6b4595a5 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/.gitignore @@ -0,0 +1 @@ +References.fsx diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.InputAsOutput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.InputAsOutput.fsx new file mode 100644 index 000000000..d39472e9c --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.InputAsOutput.fsx @@ -0,0 +1,12 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value : int } +type OutputOnly = { Value : int } + +let inputOnlyType = + Define.InputObject (name = "InputOnlyType", fields = [ Define.Input ("value", IntType) ]) + +// This should fail: InputDef cannot be assigned to OutputDef +let _ : OutputDef = ListOf inputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.OutputAsInput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.OutputAsInput.fsx new file mode 100644 index 000000000..dd3307625 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.OutputAsInput.fsx @@ -0,0 +1,12 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value : int } +type OutputOnly = { Value : int } + +let outputOnlyType = + Define.Object (name = "OutputOnlyType", fields = [ Define.Field ("value", IntType, fun _ x -> x.Value) ]) + +// This should fail: OutputDef cannot be assigned to InputDef +let _ : InputDef = ListOf outputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.InputAsOutput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.InputAsOutput.fsx new file mode 100644 index 000000000..cb37d0e38 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.InputAsOutput.fsx @@ -0,0 +1,12 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value : int } +type OutputOnly = { Value : int } + +let inputOnlyType = + Define.InputObject (name = "InputOnlyType", fields = [ Define.Input ("value", IntType) ]) + +// This should fail: InputDef cannot be assigned to OutputDef +let _ : OutputDef = Nullable inputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.OutputAsInput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.OutputAsInput.fsx new file mode 100644 index 000000000..faa94e83d --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.OutputAsInput.fsx @@ -0,0 +1,12 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value : int } +type OutputOnly = { Value : int } + +let outputOnlyType = + Define.Object (name = "OutputOnlyType", fields = [ Define.Field ("value", IntType, fun _ x -> x.Value) ]) + +// This should fail: OutputDef cannot be assigned to InputDef +let _ : InputDef = Nullable outputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.InputAsOutput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.InputAsOutput.fsx new file mode 100644 index 000000000..1d1123457 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.InputAsOutput.fsx @@ -0,0 +1,12 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value : int } +type OutputOnly = { Value : int } + +let inputOnlyType = + Define.InputObject (name = "InputOnlyType", fields = [ Define.Input ("value", IntType) ]) + +// This should fail: InputDef cannot be assigned to OutputDef +let _ : OutputDef = StructNullable inputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.OutputAsInput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.OutputAsInput.fsx new file mode 100644 index 000000000..e83c7e018 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.OutputAsInput.fsx @@ -0,0 +1,12 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value : int } +type OutputOnly = { Value : int } + +let outputOnlyType = + Define.Object (name = "OutputOnlyType", fields = [ Define.Field ("value", IntType, fun _ x -> x.Value) ]) + +// This should fail: OutputDef cannot be assigned to InputDef +let _ : InputDef = StructNullable outputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Valid.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Valid.fsx new file mode 100644 index 000000000..662d580f3 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Valid.fsx @@ -0,0 +1,20 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value : int } +type OutputOnly = { Value : int } + +let inputOnlyType = + Define.InputObject (name = "InputOnlyType", fields = [ Define.Input ("value", IntType) ]) + +let outputOnlyType = + Define.Object (name = "OutputOnlyType", fields = [ Define.Field ("value", IntType, fun _ x -> x.Value) ]) + +// These are all valid assignments and must compile successfully +let _inputList : InputDef = ListOf inputOnlyType +let _outputList : OutputDef = ListOf outputOnlyType +let _inputNullable : InputDef = Nullable inputOnlyType +let _outputNullable : OutputDef = Nullable outputOnlyType +let _inputStruct : InputDef = StructNullable inputOnlyType +let _outputStruct : OutputDef = StructNullable outputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafetyTests.fs b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafetyTests.fs new file mode 100644 index 000000000..50bc6884e --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafetyTests.fs @@ -0,0 +1,143 @@ +module FSharp.Data.GraphQL.Tests.TypeWrappersKindSafetyTests + +open System +open System.Diagnostics +open System.Threading.Tasks +open FSharp.Data.GraphQL.Types +open Xunit + +type private InputOnly = { Value : int } +type private OutputOnly = { Value : int } + +let private InputOnlyType = + Define.InputObject (name = "InputOnlyType", fields = [ Define.Input ("value", IntType) ]) + +let private OutputOnlyType = + Define.Object (name = "OutputOnlyType", fields = [ Define.Field ("value", IntType, fun _ x -> x.Value) ]) + +type TypeWrappersKindSafetyFixture () = + + let scriptsDir = IO.Path.Combine (AppContext.BaseDirectory, "TypeWrappersKindSafety") + let referencesPath = IO.Path.Combine (scriptsDir, "References.fsx") + let sourceProjectDir = + IO.Path.GetFullPath (IO.Path.Combine (AppContext.BaseDirectory, "..", "..", "..")) + let sourceScriptsDir = IO.Path.Combine (sourceProjectDir, "TypeWrappersKindSafety") + let sourceReferencesPath = IO.Path.Combine (sourceScriptsDir, "References.fsx") + + let ensureFileContentAsync (path : string) (content : string) : Task = task { + if IO.File.Exists (path) then + let! existing = IO.File.ReadAllTextAsync (path) + + if not (String.Equals (existing, content, StringComparison.Ordinal)) then + do! IO.File.WriteAllTextAsync (path, content) + else + do! IO.File.WriteAllTextAsync (path, content) + } + + member _.ScriptPath (name : string) = IO.Path.Combine (scriptsDir, name) + + member _.RunFsiCheckAsync (scriptPath : string) : Task = task { + let psi = + ProcessStartInfo ( + "dotnet", + sprintf "fsi --noninteractive \"%s\"" scriptPath, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + WorkingDirectory = AppContext.BaseDirectory + ) + + use proc = Process.Start (psi) + do! proc.WaitForExitAsync () + return proc.ExitCode + } + + member private _.ReferencesContent = + let sharedAssembly = IO.Path.Combine (AppContext.BaseDirectory, "FSharp.Data.GraphQL.Shared.dll") + let serverAssembly = IO.Path.Combine (AppContext.BaseDirectory, "FSharp.Data.GraphQL.Server.dll") + + [| sprintf "#r @\"%s\"" sharedAssembly; sprintf "#r @\"%s\"" serverAssembly |] + |> String.concat "\n" + + interface IAsyncLifetime with + + member this.InitializeAsync () : Task = task { + IO.Directory.CreateDirectory (scriptsDir) |> ignore + IO.Directory.CreateDirectory (sourceScriptsDir) |> ignore + + let content = this.ReferencesContent + + do! ensureFileContentAsync referencesPath content + do! ensureFileContentAsync sourceReferencesPath content + } + + member _.DisposeAsync () = Task.CompletedTask + +type TypeWrappersKindSafetyTests (fixture : TypeWrappersKindSafetyFixture) = + interface IClassFixture + + [] + member _.``ListOf keeps input-output direction`` () : Task = task { + let inputList : InputDef = ListOf InputOnlyType + let outputList : OutputDef = ListOf OutputOnlyType + Assert.Equal ("[InputOnlyType!]!", inputList.ToString ()) + Assert.Equal ("[OutputOnlyType!]!", outputList.ToString ()) + } + + [] + member _.``Nullable keeps input-output direction`` () : Task = task { + let nullableInput : InputDef = Nullable InputOnlyType + let nullableOutput : OutputDef = Nullable OutputOnlyType + Assert.Equal ("InputOnlyType", nullableInput.ToString ()) + Assert.Equal ("OutputOnlyType", nullableOutput.ToString ()) + } + + [] + member _.``StructNullable keeps input-output direction`` () : Task = task { + let nullableInput : InputDef = StructNullable InputOnlyType + let nullableOutput : OutputDef = StructNullable OutputOnlyType + Assert.Equal ("InputOnlyType", nullableInput.ToString ()) + Assert.Equal ("OutputOnlyType", nullableOutput.ToString ()) + } + + [] + member _.``Valid script compiles successfully`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("Valid.fsx")) + Assert.Equal (0, exitCode) + } + + [] + member _.``ListOf rejects output type as input at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("ListOf.OutputAsInput.fsx")) + Assert.NotEqual (0, exitCode) + } + + [] + member _.``ListOf rejects input type as output at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("ListOf.InputAsOutput.fsx")) + Assert.NotEqual (0, exitCode) + } + + [] + member _.``Nullable rejects output type as input at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("Nullable.OutputAsInput.fsx")) + Assert.NotEqual (0, exitCode) + } + + [] + member _.``Nullable rejects input type as output at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("Nullable.InputAsOutput.fsx")) + Assert.NotEqual (0, exitCode) + } + + [] + member _.``StructNullable rejects output type as input at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("StructNullable.OutputAsInput.fsx")) + Assert.NotEqual (0, exitCode) + } + + [] + member _.``StructNullable rejects input type as output at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("StructNullable.InputAsOutput.fsx")) + Assert.NotEqual (0, exitCode) + } diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputComplexTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputComplexTests.fs index 954ace7ec..1e7053574 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputComplexTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputComplexTests.fs @@ -46,7 +46,7 @@ type TestInput = optArr : string option array option voptArr : string option array voption } // string voption array voption is too hard to implement -let InputArrayOf (innerDef : #TypeDef<'Val>) : ListOfDef<'Val, 'Val array> = ListOf innerDef +let InputArrayOf (innerDef : #InputDef<'Val>) : InputDef<'Val array> = ListOf (innerDef :> InputDef<'Val>) let TestInputObject = Define.InputObject ( diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs index 69fc2b825..de5730092 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs @@ -14,7 +14,7 @@ open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Shared -let InputArrayOf (innerDef : #TypeDef<'Val>) : ListOfDef<'Val, 'Val array> = ListOf innerDef +let InputArrayOf (innerDef : #InputDef<'Val>) : InputDef<'Val array> = ListOf (innerDef :> InputDef<'Val>) let TestInputObject = InputComplexTests.TestInputObject From 075018bdd7d17a62150003ccd47e86a550f380c9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 23:08:59 +0200 Subject: [PATCH 03/32] Pin CI workflows and build script to .NET SDK 10.0.300 (#578) Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Andrii Chebukin --- .github/workflows/publish-ci.yml | 2 +- .github/workflows/publish-release.yml | 2 +- .github/workflows/pull-request.yml | 2 +- build/Program.fs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-ci.yml b/.github/workflows/publish-ci.yml index f31487b5c..e141251e7 100644 --- a/.github/workflows/publish-ci.yml +++ b/.github/workflows/publish-ci.yml @@ -8,7 +8,7 @@ on: env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true - DOTNET_SDK_VERSION: 10.0.202 + DOTNET_SDK_VERSION: 10.0.300 jobs: publish: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index d4df8454f..a73e62012 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -9,7 +9,7 @@ env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true SLEEP_DURATION: 60 - DOTNET_SDK_VERSION: 10.0.202 + DOTNET_SDK_VERSION: 10.0.300 jobs: publish: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 07356a39c..7befa633e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -21,7 +21,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-22.04, windows-latest, macOS-latest] - dotnet: [10.0.202] + dotnet: [10.0.300] runs-on: ${{ matrix.os }} steps: diff --git a/build/Program.fs b/build/Program.fs index f23d68415..9624a0470 100644 --- a/build/Program.fs +++ b/build/Program.fs @@ -30,7 +30,7 @@ let ctx = Context.forceFakeContext () let embedAll = ctx.Arguments |> List.exists (fun arg -> arg = BuildArguments.EmbedAll) module DotNetCli = - let setVersion (o : DotNet.Options) = { o with Version = Some "10.0.202" } + let setVersion (o : DotNet.Options) = { o with Version = Some "10.0.300" } let setRestoreOptions (o : DotNet.RestoreOptions) = o.WithCommon setVersion let configurationString = Environment.environVarOrDefault "CONFIGURATION" "Release" From 19d11c3f1718b0d43a2b88b2d24425bd72b54b7e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 13 Jun 2026 19:43:56 +0200 Subject: [PATCH 04/32] Pin CI workflows and build script to .NET SDK 10.0.301 --- .github/workflows/publish-ci.yml | 2 +- .github/workflows/publish-release.yml | 2 +- .github/workflows/pull-request.yml | 2 +- build/Program.fs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-ci.yml b/.github/workflows/publish-ci.yml index e141251e7..3b26ec16a 100644 --- a/.github/workflows/publish-ci.yml +++ b/.github/workflows/publish-ci.yml @@ -8,7 +8,7 @@ on: env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true - DOTNET_SDK_VERSION: 10.0.300 + DOTNET_SDK_VERSION: 10.0.301 jobs: publish: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index a73e62012..c329f6816 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -9,7 +9,7 @@ env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true SLEEP_DURATION: 60 - DOTNET_SDK_VERSION: 10.0.300 + DOTNET_SDK_VERSION: 10.0.301 jobs: publish: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7befa633e..3397e91f1 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -21,7 +21,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-22.04, windows-latest, macOS-latest] - dotnet: [10.0.300] + dotnet: [10.0.301] runs-on: ${{ matrix.os }} steps: diff --git a/build/Program.fs b/build/Program.fs index 9624a0470..517ece4b4 100644 --- a/build/Program.fs +++ b/build/Program.fs @@ -30,7 +30,7 @@ let ctx = Context.forceFakeContext () let embedAll = ctx.Arguments |> List.exists (fun arg -> arg = BuildArguments.EmbedAll) module DotNetCli = - let setVersion (o : DotNet.Options) = { o with Version = Some "10.0.300" } + let setVersion (o : DotNet.Options) = { o with Version = Some "10.0.301" } let setRestoreOptions (o : DotNet.RestoreOptions) = o.WithCommon setVersion let configurationString = Environment.environVarOrDefault "CONFIGURATION" "Release" From cc5fc0f7b038a096d94d84d9fe393fa48307fb55 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 13 Jun 2026 19:45:24 +0200 Subject: [PATCH 05/32] Fixed Repo Assist incorrectly encoded characters --- .github/workflows/repo-assist.md | 103 ++++++++++++++++--------------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/.github/workflows/repo-assist.md b/.github/workflows/repo-assist.md index 2e34b820e..07de8952a 100644 --- a/.github/workflows/repo-assist.md +++ b/.github/workflows/repo-assist.md @@ -1,6 +1,7 @@ ---- +--- description: | A friendly repository assistant that runs daily to support contributors and maintainers. + - Comments helpfully on open issues to unblock contributors and onboard newcomers - Identifies issues that can be fixed and creates draft pull requests with fixes - Studies the codebase and proposes improvements via PRs @@ -58,6 +59,7 @@ tools: repo-memory: true steps: + - name: Checkout repository uses: actions/checkout@v5 with: @@ -87,7 +89,7 @@ Always be: - **Concise**: Keep comments focused and actionable. Avoid walls of text. - **Mindful of project values**: Prioritize **stability**, **correctness**, and **minimal dependencies**. Do not introduce new dependencies without clear justification. - **Transparent about your nature**: Always clearly identify yourself as Repo Assist, an automated AI assistant. Never pretend to be a human maintainer. -- **Restrained**: When in doubt, do nothing. It is always better to stay silent than to post a redundant, unhelpful, or spammy comment. Human maintainers' attention is precious do not waste it. +- **Restrained**: When in doubt, do nothing. It is always better to stay silent than to post a redundant, unhelpful, or spammy comment. Human maintainers' attention is precious — do not waste it. ## Memory @@ -103,7 +105,7 @@ At the **end** of every run, update your repo memory with a summary of what you ## Workflow -Each run, work through these tasks in order. Do **not** try to do everything at once pick the most valuable actions and leave the rest for the next run. +Each run, work through these tasks in order. Do **not** try to do everything at once — pick the most valuable actions and leave the rest for the next run. Always do Task 10 (Update Monthly Activity Summary Issue) in addition to any other tasks you perform. @@ -115,8 +117,8 @@ Note: In issue comments and PR descriptions, identify yourself as "Repo Assist". 1. List open issues in the repository (most recently updated first). 2. For each issue (up to 10): - a. **Check your memory first**: Have you already commented on this issue? If yes, **skip it entirely** do not post follow-up comments unless explicitly requested by a human in the thread. - b. Has a human maintainer or contributor already provided a helpful response? If yes, **skip it** do not duplicate or rephrase their input. + a. **Check your memory first**: Have you already commented on this issue? If yes, **skip it entirely** — do not post follow-up comments unless explicitly requested by a human in the thread. + b. Has a human maintainer or contributor already provided a helpful response? If yes, **skip it** — do not duplicate or rephrase their input. c. Read the issue carefully. d. Determine the issue type: - **Bug report**: Acknowledge the problem, ask for a minimal reproduction if not already provided, or suggest a likely cause if you can identify one from the code. @@ -134,7 +136,7 @@ Note: In issue comments and PR descriptions, identify yourself as "Repo Assist". - Restatements of what the issue author already said - Follow-ups to your own previous comments g. **AI Disclosure**: Begin every comment with a brief disclosure, e.g.: - > ?? *This is an automated response from Repo Assist, the repository's AI assistant.* + > 🤖 *This is an automated response from Repo Assist, the repository's AI assistant.* 3. Update your memory to note which issues you commented on. **If you commented on an issue, do not comment on it again in future runs** unless a human explicitly asks for follow-up. ### Task 2: Fix Issues via Pull Requests @@ -143,13 +145,13 @@ Note: In issue comments and PR descriptions, identify yourself as "Repo Assist". 1. Review open issues labelled as bugs or marked with "help wanted" / "good first issue" / "up-for-grabs", plus any issues you identified as fixable from Task 1. 2. For each fixable issue: - a. Check your memory: have you already tried to fix this issue? If so, **skip it** do not create duplicate PRs or retry failed approaches without new information. + a. Check your memory: have you already tried to fix this issue? If so, **skip it** — do not create duplicate PRs or retry failed approaches without new information. b. **Create a fresh branch**: Each PR must be independent, based off the latest `dev` branch, using a unique branch name (e.g., `repo-assist/fix-issue-123-`). c. Study the relevant code carefully before making changes. d. Implement a minimal, surgical fix. Do **not** refactor unrelated code. e. **Build and test (MANDATORY)**: - - Run the project's build command if this fails, **do not create a PR**. Fix the issue or abandon the attempt. - - Run the project's test command all tests must pass before proceeding. + - Run the project's build command — if this fails, **do not create a PR**. Fix the issue or abandon the attempt. + - Run the project's test command — all tests must pass before proceeding. - If tests fail due to your changes, fix them or abandon the PR attempt. - If tests fail due to environment/infrastructure issues (not your changes), you may still create the PR but **must document this clearly** (see below). f. Add a new test that covers the bug if appropriate and feasible. Run tests again after adding. @@ -157,13 +159,13 @@ Note: In issue comments and PR descriptions, identify yourself as "Repo Assist". - All tests pass, OR - Tests could not run due to environment issues (not your code) h. Create a draft pull request. In the PR description: - - **Start with AI disclosure**: Begin with "?? *Repo Assist here I'm an automated AI assistant for this repository.*" + - **Start with AI disclosure**: Begin with "🤖 *Repo Assist here — I'm an automated AI assistant for this repository.*" - Link the issue it addresses (e.g., "Closes #123") - Explain the root cause and the fix - Note any trade-offs - **Test status (REQUIRED)**: Include a section like: - ``` + ```text ## Test Status - [x] Build passes - [x] Tests pass @@ -171,7 +173,7 @@ Note: In issue comments and PR descriptions, identify yourself as "Repo Assist". Or if tests could not run: - ``` + ```text ## Test Status - [x] Build passes - [ ] Tests could not be run: [explain environment/infrastructure issue] @@ -193,7 +195,7 @@ Note: In issue comments and PR descriptions, identify yourself as "Repo Assist". - Code clarity and maintainability improvements 3. For each improvement, **create a fresh branch** based off the latest `dev` branch with a unique name (e.g., `repo-assist/improve-`). 4. Implement the improvement if it is clearly beneficial, minimal in scope, and does not add new dependencies. -5. **Build and test (MANDATORY)** same requirements as Task 2: +5. **Build and test (MANDATORY)** — same requirements as Task 2: - Do not create a PR if any build fails or if any tests fail due to your changes - Document test status in the PR description 6. Create a draft PR with a clear description explaining the rationale. **Include the AI disclosure** and **Test Status section** at the start of the PR description. @@ -209,13 +211,13 @@ Keep the project's dependencies and build tooling current. This reduces technica a. Prefer minor and patch updates. Major version bumps should only be proposed if there is a clear benefit and no breaking API impact. b. **Create a fresh branch** based off the latest `dev` branch with a unique name (e.g., `repo-assist/deps-update-`). c. Update the relevant dependency file(s). - d. **Build and test (MANDATORY)** same requirements as Task 2. + d. **Build and test (MANDATORY)** — same requirements as Task 2. e. Create a draft PR describing which packages were updated and why. Include the **Test Status section**. 3. **Engineering improvements**: Look for other engineering updates such as: - Updating CI/build tooling - Modernising project file patterns - Updating SDK or runtime versions -4. **Build and test (MANDATORY)** for all changes same requirements as Task 2. +4. **Build and test (MANDATORY)** for all changes — same requirements as Task 2. 5. Update your memory with what you checked/updated and when. ### Task 5: Maintain Repo Assist Pull Requests @@ -227,7 +229,7 @@ Keep PRs created by Repo Assist in a healthy state by fixing CI failures and res a. **Check CI status**: If CI is failing due to your changes, investigate the failure, fix the code, and push updates using the `push_to_pull_request_branch` tool. b. **Check for merge conflicts**: If the PR has merge conflicts with the base branch, rebase or merge the base branch and resolve conflicts, then push the updated branch. c. **Check your memory**: If you have already attempted to fix this PR multiple times without success, add a comment explaining the situation and leave it for human review. -3. Do not push updates to PRs that are failing due to unrelated infrastructure issues document those in a comment instead. +3. Do not push updates to PRs that are failing due to unrelated infrastructure issues — document those in a comment instead. 4. Update your memory with which PRs you updated. ### Task 6: Stale PR Nudges @@ -236,13 +238,13 @@ Help move stalled PRs forward by politely nudging authors when PRs are blocked w 1. List open PRs that have not been updated in 14+ days. 2. For each stale PR: - a. **Check your memory**: Have you already nudged this PR? If yes, skip it do not repeatedly nag. + a. **Check your memory**: Have you already nudged this PR? If yes, skip it — do not repeatedly nag. b. **Check the context**: Is the PR waiting for the author to respond to review feedback, fix CI, or address requested changes? c. If the PR is blocked on the author, post a single, polite comment: - > ?? *Friendly nudge from Repo Assist* + > 🤖 *Friendly nudge from Repo Assist* > > Hi @! This PR has been waiting for updates. Is there anything blocking you, or would you like help resolving the outstanding items? If you're no longer working on this, please let us know so we can close it or find another contributor to take over. - d. If the PR is blocked on maintainer review (not the author), do **not** comment that's not your job. + d. If the PR is blocked on maintainer review (not the author), do **not** comment — that's not your job. 3. Update your memory to note which PRs you nudged and when. 4. **Maximum nudges per run**: 3. Do not spam. @@ -253,10 +255,10 @@ Keep issues and PRs well-organized by applying appropriate labels based on conte 1. Review recently created or updated issues and PRs that lack labels. 2. For each unlabeled item: a. Analyze the content to determine the appropriate labels: - - `bug` for bug reports or PRs fixing bugs - - `enhancement` for feature requests or PRs adding features - - `help wanted` for issues where external help would be valuable - - `good first issue` for issues suitable for newcomers (simple, well-documented, isolated) + - `bug` — for bug reports or PRs fixing bugs + - `enhancement` — for feature requests or PRs adding features + - `help wanted` — for issues where external help would be valuable + - `good first issue` — for issues suitable for newcomers (simple, well-documented, isolated) b. Apply labels using the `add_labels` tool. c. Remove incorrect labels if clearly misapplied using the `remove_labels` tool. 3. **Be conservative**: Only apply labels you are confident about. When in doubt, skip. @@ -273,7 +275,7 @@ Help maintainers prepare releases by keeping changelogs up to date and proposing a. Determine the appropriate version bump following [SemVer](https://semver.org/): - **Patch** (e.g., 1.2.3 > 1.2.4): Bug fixes, docs, internal improvements - **Minor** (e.g., 1.2.3 > 1.3.0): New features, backwards-compatible additions - - **Major** (e.g., 1.2.3 > 2.0.0): Breaking changes **never propose without maintainer approval** + - **Major** (e.g., 1.2.3 > 2.0.0): Breaking changes — **never propose without maintainer approval** b. **Create a fresh branch** based off the latest `dev` branch (e.g., `repo-assist/release-vX.Y.Z`). c. Update the changelog file with entries for each merged PR, following the existing format. d. Create a draft PR with: @@ -295,15 +297,18 @@ Make new contributors feel welcome with a friendly greeting on their first PR or a. Search for previous PRs or issues by the same author. b. If this is their **first contribution** to the repository: - Post a warm welcome comment: - > ?? *Welcome from Repo Assist!* + > 🤖 *Welcome from Repo Assist!* > - > Hi @! ?? Thanks for your first contribution to this project! We're excited to have you here. + > Hi @! 👋 Thanks for your first contribution to this project! We're excited to have you here. > > A few helpful resources: - > - ?? [README](README.md) Project overview and getting started - > - ?? [Contributing Guide](CONTRIBUTING.md) How to contribute (if it exists) + > + > + > - 📖 [README](README.md) — Project overview and getting started + > - 🤝 [Contributing Guide](CONTRIBUTING.md) — How to contribute (if it exists) > > A maintainer will review your contribution soon. Feel free to ask if you have any questions! + 3. **Check your memory** first: Do not welcome the same contributor twice. 4. **Maximum welcomes per run**: 3. Avoid flooding. 5. Update your memory with welcomed contributors. @@ -319,28 +324,28 @@ Maintain a single open issue titled `[Repo Assist] Monthly Activity {YYYY}-{MM}` 2. **Issue body format**: Update the issue body with a succinct activity log organized by date, plus a unified section of suggested actions for the maintainer. Use the following structure: ```markdown - ?? *Repo Assist here I'm an automated AI assistant for this repository.* + 🤖 *Repo Assist here — I'm an automated AI assistant for this repository.* ## Activity for ### - - ?? Commented on #: - - ?? Created PR #: - - ??? Labelled # with ` - + From bdd729778185f81614c0973189627b084de5565b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 14 Jun 2026 04:21:00 +0200 Subject: [PATCH 10/32] fixup! Updated `FSharp.TypeProviders.SDK` to `8.10.0` --- .../FSharp.Data.GraphQL.Client.fsproj | 14 +++++++++++--- .../GraphQLProvider.Runtime.fs | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Client/FSharp.Data.GraphQL.Client.fsproj b/src/FSharp.Data.GraphQL.Client/FSharp.Data.GraphQL.Client.fsproj index f25215ca6..46901b937 100644 --- a/src/FSharp.Data.GraphQL.Client/FSharp.Data.GraphQL.Client.fsproj +++ b/src/FSharp.Data.GraphQL.Client/FSharp.Data.GraphQL.Client.fsproj @@ -36,6 +36,8 @@ it directly as a TfmSpecificPackageFile. --> $(TargetsForTfmSpecificContentInPackage);_IncludeDesignTimeDllInPackage + + $(NoWarn);NU5100 @@ -46,11 +48,17 @@ - + + + + + - - lib\$(TargetFramework) + + typeproviders\fsharp41\$(TargetFramework) diff --git a/src/FSharp.Data.GraphQL.Client/GraphQLProvider.Runtime.fs b/src/FSharp.Data.GraphQL.Client/GraphQLProvider.Runtime.fs index 40d501fcd..51588dbc0 100644 --- a/src/FSharp.Data.GraphQL.Client/GraphQLProvider.Runtime.fs +++ b/src/FSharp.Data.GraphQL.Client/GraphQLProvider.Runtime.fs @@ -6,6 +6,6 @@ namespace FSharp.Data.GraphQL.Client open FSharp.Core.CompilerServices -[] +[] do () #endif From 15df50a655eebbebc9690e60b3f59899d2f905e1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:59:36 +0200 Subject: [PATCH 11/32] High severity vulnerability fix: upgrade `SQLitePCLRaw.lib.e_sqlite3` to `3.53.3` (#588) Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- Packages.props | 1 + samples/relay-book-store/relay-book-store.fsproj | 1 + 2 files changed, 2 insertions(+) diff --git a/Packages.props b/Packages.props index 323f603f2..4f5545f2d 100644 --- a/Packages.props +++ b/Packages.props @@ -80,6 +80,7 @@ + diff --git a/samples/relay-book-store/relay-book-store.fsproj b/samples/relay-book-store/relay-book-store.fsproj index 492a31de9..d2afed9b9 100644 --- a/samples/relay-book-store/relay-book-store.fsproj +++ b/samples/relay-book-store/relay-book-store.fsproj @@ -14,6 +14,7 @@ + From c1e85105ba1a43b25eea35d1dfd8cc4e612f8bc5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:09:46 +0200 Subject: [PATCH 12/32] Support case-insensitive comparison with `ObjectListFilter` (#582) Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Andrii Chebukin --- README.md | 14 +- RELEASE_NOTES.md | 1 + ...harp.Data.GraphQL.Server.Middleware.fsproj | 1 + .../FilterSuffixConstants.fs | 67 +++++++++ .../ObjectListFilter.fs | 137 ++++++++++++------ .../SchemaDefinitions.fs | 69 ++++++--- .../MiddlewareTests.fs | 24 +-- .../ObjectListFilterLinqGenerateTests.fs | 58 +++++--- .../ObjectListFilterLinqTests.fs | 118 +++++++++++++-- 9 files changed, 378 insertions(+), 111 deletions(-) create mode 100644 src/FSharp.Data.GraphQL.Server.Middleware/FilterSuffixConstants.fs diff --git a/README.md b/README.md index 39fd14c8a..3d5b62d70 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,8 @@ query TestQuery { } ``` +For string filters, lowercase suffixes are case-insensitive (`name_starts_with`, `name_sw`, `name_contains`, `name_equals`, `name_eq`), while capitalized suffixes are case-sensitive (`name_Starts_With`, `name_SW`, `name_Contains`, `name_Equals`, `name_EQ`). `contains`/`Contains` do not have shorthand aliases. + Also you can apply `not` operator like this: ```graphql @@ -406,12 +408,16 @@ type ObjectListFilter = | And of ObjectListFilter * ObjectListFilter | Or of ObjectListFilter * ObjectListFilter | Not of ObjectListFilter - | Equals of FieldFilter + | Equals of Filter : FieldFilter * Comparer : System.Collections.IComparer | GreaterThan of FieldFilter + | GreaterThanOrEqual of FieldFilter | LessThan of FieldFilter - | StartsWith of FieldFilter - | EndsWith of FieldFilter - | Contains of FieldFilter + | LessThanOrEqual of FieldFilter + | In of FieldFilter + | StartsWith of Filter : FieldFilter * Comparer : System.StringComparer + | EndsWith of Filter : FieldFilter * Comparer : System.StringComparer + | Contains of Filter : FieldFilter * Comparer : System.Collections.IComparer + | OfTypes of System.Type list | FilterField of FieldFilter ``` diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 948a801cd..059e705c2 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -288,4 +288,5 @@ * **Breaking Change** Migrated to .NET 10 * **Breaking Change** Made Relay `Edge` a read-only struct +* Added case-insensitive string comparison support to `ObjectListFilter`, including comparer-aware filter cases and GraphQL filter suffix handling * Improved Relay XML documentation comments diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj b/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj index 09357bf2b..9eddcf46f 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj +++ b/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj @@ -21,6 +21,7 @@ + diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/FilterSuffixConstants.fs b/src/FSharp.Data.GraphQL.Server.Middleware/FilterSuffixConstants.fs new file mode 100644 index 000000000..3e376f281 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.Middleware/FilterSuffixConstants.fs @@ -0,0 +1,67 @@ +/// +/// String filter suffixes: +/// lowercase (e.g. _ends_with, _ew) → case-insensitive (OrdinalIgnoreCase) +/// Capitalized (e.g. _Ends_With, _EW) → case-sensitive (Ordinal) +/// +/// The submodule contains lowercase suffixes that map to case-insensitive string comparisons. +/// The submodule contains capitalized/uppercase suffixes that map to case-sensitive string comparisons. +/// Numeric and comparison operator suffixes are defined at the module level and are case-insensitive by convention. +/// +/// +[] +module FSharp.Data.GraphQL.Server.Middleware.FilterSuffixConstants + +// Numeric/comparison operators +[] +let GreaterThanOrEqualSuffix = "_greater_than_or_equal" +[] +let GTESuffix = "_gte" +[] +let GreaterThanSuffix = "_greater_than" +[] +let GTSuffix = "_gt" +[] +let LessThanOrEqualSuffix = "_less_than_or_equal" +[] +let LTESuffix = "_lte" +[] +let LessThanSuffix = "_less_than" +[] +let LTSuffix = "_lt" +[] +let InSuffix = "_in" + +/// Case-insensitive string operators and all numeric/comparison operators +module CI = + // String operators (case-insensitive) + [] + let EndsWithSuffix = "_ends_with" + [] + let EWSuffix = "_ew" + [] + let StartsWithSuffix = "_starts_with" + [] + let SWSuffix = "_sw" + [] + let ContainsSuffix = "_contains" + [] + let EqualsSuffix = "_equals" + [] + let EQSuffix = "_eq" + +/// Case-sensitive string operators +module CS = + [] + let EndsWithSuffix = "_Ends_With" + [] + let EWSuffix = "_EW" + [] + let StartsWithSuffix = "_Starts_With" + [] + let SWSuffix = "_SW" + [] + let ContainsSuffix = "_Contains" + [] + let EqualsSuffix = "_Equals" + [] + let EQSuffix = "_EQ" diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 12f54d712..bb4f80e47 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -1,25 +1,36 @@ namespace FSharp.Data.GraphQL.Server.Middleware open System +open System.Collections open FSharp.Data.GraphQL /// A filter definition for a field value. type FieldFilter<'Val> = { FieldName : string; Value : 'Val } +/// /// A filter definition for an object list. +/// +/// +/// String-based filters can carry a comparer. When the comparer is not provided by the default +/// string operators, `StartsWith`, `EndsWith`, and string `Contains` preserve the existing +/// case-sensitive `StringComparison.CurrentCulture` behavior. +/// `StringComparer.CurrentCultureIgnoreCase` enables case-insensitive matching. +/// When filters are provided through GraphQL input, lowercase string suffixes are interpreted +/// as case-insensitive and capitalized suffixes are interpreted as case-sensitive. +/// type ObjectListFilter = | And of ObjectListFilter * ObjectListFilter | Or of ObjectListFilter * ObjectListFilter | Not of ObjectListFilter - | Equals of FieldFilter - | GreaterThan of FieldFilter - | GreaterThanOrEqual of FieldFilter - | LessThan of FieldFilter - | LessThanOrEqual of FieldFilter + | Equals of Filter : FieldFilter * Comparer : IComparer + | GreaterThan of FieldFilter + | GreaterThanOrEqual of FieldFilter + | LessThan of FieldFilter + | LessThanOrEqual of FieldFilter | In of FieldFilter - | StartsWith of FieldFilter - | EndsWith of FieldFilter - | Contains of FieldFilter + | StartsWith of Filter : FieldFilter * Comparer : StringComparer + | EndsWith of Filter : FieldFilter * Comparer : StringComparer + | Contains of Filter : FieldFilter * Comparer : IComparer | OfTypes of Type list | FilterField of FieldFilter @@ -95,7 +106,7 @@ module ObjectListFilter = let ( ||| ) x y = Or (x, y) /// Creates a new ObjectListFilter representing an EQUALS operation between two comparable values. - let ( === ) fname value = Equals { FieldName = fname; Value = value } + let ( === ) fname value = Equals ({ FieldName = fname; Value = value }, null) /// Creates a new ObjectListFilter representing a GREATER THAN operation of a comparable value. let ( >>> ) fname value = GreaterThan { FieldName = fname; Value = value } @@ -110,13 +121,13 @@ module ObjectListFilter = let ( <== ) fname value = LessThanOrEqual { FieldName = fname; Value = value } /// Creates a new ObjectListFilter representing a STARTS WITH operation of a string value. - let ( =@@ ) fname value = StartsWith { FieldName = fname; Value = value } + let ( =@@ ) fname value = StartsWith ({ FieldName = fname; Value = value }, null) /// Creates a new ObjectListFilter representing an ENDS WITH operation of a string value. - let ( @@= ) fname value = EndsWith { FieldName = fname; Value = value } + let ( @@= ) fname value = EndsWith ({ FieldName = fname; Value = value }, null) /// Creates a new ObjectListFilter representing a CONTAINS operation. - let ( @=@ ) fname value = Contains { FieldName = fname; Value = value } + let ( @=@ ) fname value = Contains ({ FieldName = fname; Value = value }, null) /// Creates a new ObjectListFilter representing a IN operation. let ( =~= ) fname value = In { FieldName = fname; Value = value } @@ -124,9 +135,21 @@ module ObjectListFilter = /// Creates a new ObjectListFilter representing a field sub comparison. let ( --> ) fname filter = FilterField { FieldName = fname; Value = filter } - /// Creates a new ObjectListFilter representing a NOT opreation for the existing one. + /// Creates a new ObjectListFilter representing a NOT operation for the existing one. let ( !!! ) filter = Not filter + /// Creates a new ObjectListFilter representing a case-insensitive EQUALS operation on a string value. + let ( ===~ ) fname (value : string) = Equals ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + + /// Creates a new ObjectListFilter representing a case-insensitive STARTS WITH operation on a string value. + let ( =@@~ ) fname (value : string) = StartsWith ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + + /// Creates a new ObjectListFilter representing a case-insensitive ENDS WITH operation on a string value. + let ( @@=~ ) fname (value : string) = EndsWith ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + + /// Creates a new ObjectListFilter representing a case-insensitive CONTAINS operation on a string value. + let ( @=@~ ) fname (value : string) = Contains ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + let private genericWhereMethod = typeof.GetMethods () |> Seq.where (fun m -> m.Name = "Where") @@ -144,9 +167,11 @@ module ObjectListFilter = let private stringType = typeof let private genericIEnumerableType = typedefof> - let private StringStartsWithMethod = stringType.GetMethod ("StartsWith", [| stringType |]) - let private StringEndsWithMethod = stringType.GetMethod ("EndsWith", [| stringType |]) - let private StringContainsMethod = stringType.GetMethod ("Contains", [| stringType |]) + let private stringComparisonType = typeof + let private StringStartsWithMethod = stringType.GetMethod ("StartsWith", [| stringType; stringComparisonType |]) + let private StringEndsWithMethod = stringType.GetMethod ("EndsWith", [| stringType; stringComparisonType |]) + let private StringContainsMethod = stringType.GetMethod ("Contains", [| stringType; stringComparisonType |]) + let private StringEqualsMethod = stringType.GetMethod ("Equals", [| stringType; stringComparisonType |]) let private unwrapOptionMethod = FSharp.Data.GraphQL.Helpers.moduleType.GetMethod (nameof Helpers.unwrap) @@ -205,6 +230,21 @@ module ObjectListFilter = |> Seq.where (fun m -> m.Name = "Equals") |> Seq.head + /// Maps an IComparer to a StringComparison value. + /// Returns ValueNone only when the comparer is null or is not a recognized StringComparer. + let private comparerToStringComparison (comparer : IComparer) = + match comparer with + | null -> ValueNone + | :? StringComparer as sc -> + if obj.ReferenceEquals (sc, StringComparer.OrdinalIgnoreCase) then ValueSome StringComparison.OrdinalIgnoreCase + elif obj.ReferenceEquals (sc, StringComparer.InvariantCultureIgnoreCase) then ValueSome StringComparison.InvariantCultureIgnoreCase + elif obj.ReferenceEquals (sc, StringComparer.CurrentCultureIgnoreCase) then ValueSome StringComparison.CurrentCultureIgnoreCase + elif obj.ReferenceEquals (sc, StringComparer.Ordinal) then ValueSome StringComparison.Ordinal + elif obj.ReferenceEquals (sc, StringComparer.InvariantCulture) then ValueSome StringComparison.InvariantCulture + elif obj.ReferenceEquals (sc, StringComparer.CurrentCulture) then ValueSome StringComparison.CurrentCulture + else ValueNone + | _ -> ValueNone + let rec buildFilterExpr isEnumerableQuery (param : SourceExpression) buildTypeDiscriminatorCheck filter : Expression = let build = buildFilterExpr isEnumerableQuery param buildTypeDiscriminatorCheck @@ -224,31 +264,41 @@ module ObjectListFilter = | _ -> Expression.Convert (``member``, stringType) match filter with - | Not (Equals f) -> + | Not (Equals (f, comparer)) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let hasEqualityOperator = hasEqualityOperator ``member``.Type - match f.Value with - | NoCast when hasEqualityOperator -> Expression.NotEqual (``member``, Expression.Constant f.Value) - | NoCast - | NonEnumerableCast _ -> - Expression.NotEqual (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) - | Enumerable -> - let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) - Expression.Not (Expression.Call (``const``, equalsMethod, ``member``)) + match comparerToStringComparison comparer with + | ValueSome comparison -> + let value = Helpers.unwrap (box f.Value) :?> string + Expression.Not (Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsMethod, Expression.Constant (value, typeof), Expression.Constant comparison)) :> Expression + | ValueNone -> + let hasEqualityOperator = hasEqualityOperator ``member``.Type + match f.Value with + | NoCast when hasEqualityOperator -> Expression.NotEqual (``member``, Expression.Constant f.Value) + | NoCast + | NonEnumerableCast _ -> + Expression.NotEqual (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) + | Enumerable -> + let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) + Expression.Not (Expression.Call (``const``, equalsMethod, ``member``)) | Not f -> f |> build |> Expression.Not :> Expression | And (f1, f2) -> Expression.AndAlso (build f1, build f2) | Or (f1, f2) -> Expression.OrElse (build f1, build f2) - | Equals f -> + | Equals (f, comparer) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let hasEqualityOperator = hasEqualityOperator ``member``.Type - match f.Value with - | NoCast when hasEqualityOperator -> Expression.Equal (``member``, Expression.Constant f.Value) - | NoCast - | NonEnumerableCast _ -> - Expression.Equal (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) - | Enumerable -> - let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) - Expression.Call (``const``, equalsMethod, ``member``) + match comparerToStringComparison comparer with + | ValueSome comparison -> + let value = Helpers.unwrap (box f.Value) :?> string + Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsMethod, Expression.Constant (value, typeof), Expression.Constant comparison) :> Expression + | ValueNone -> + let hasEqualityOperator = hasEqualityOperator ``member``.Type + match f.Value with + | NoCast when hasEqualityOperator -> Expression.Equal (``member``, Expression.Constant f.Value) + | NoCast + | NonEnumerableCast _ -> + Expression.Equal (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) + | Enumerable -> + let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) + Expression.Call (``const``, equalsMethod, ``member``) | GreaterThan f -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) match f.Value with @@ -273,14 +323,16 @@ module ObjectListFilter = | NoCast -> Expression.LessThanOrEqual (``member``, Expression.Constant f.Value) | Enumerable -> Expression.LessThanOrEqual (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) | NonEnumerableCast ``type`` -> Expression.LessThanOrEqual ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) - | StartsWith f -> + | StartsWith (f, comparer) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithMethod, Expression.Constant f.Value) - | EndsWith f -> + let comparison = comparerToStringComparison (comparer :> IComparer) |> ValueOption.defaultValue StringComparison.CurrentCulture + Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithMethod, Expression.Constant f.Value, Expression.Constant comparison) + | EndsWith (f, comparer) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithMethod, Expression.Constant f.Value) + let comparison = comparerToStringComparison (comparer :> IComparer) |> ValueOption.defaultValue StringComparison.CurrentCulture + Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithMethod, Expression.Constant f.Value, Expression.Constant comparison) - | Contains f -> + | Contains (f, comparer) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) let isEnumerable (memberType : Type) = not (Type.(=) (memberType, stringType)) @@ -316,7 +368,8 @@ module ObjectListFilter = | :? FieldInfo as field when field.FieldType |> isEnumerable -> callContains field.FieldType | _ -> let unwrappedValue = Helpers.unwrap f.Value - Expression.Call (normalizeStringMemberExpr ``member``, StringContainsMethod, Expression.Constant unwrappedValue) + let comparison = comparerToStringComparison comparer |> ValueOption.defaultValue StringComparison.CurrentCulture + Expression.Call (normalizeStringMemberExpr ``member``, StringContainsMethod, Expression.Constant (unwrappedValue :?> string, typeof), Expression.Constant comparison) | In f when not (f.Value.IsEmpty) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) let enumerableContains = getEnumerableContainsMethod objectType diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs index e8239cad9..813c3b7bb 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs @@ -9,9 +9,10 @@ open FSharp.Data.GraphQL.Ast open FsToolkit.ErrorHandling type private ComparisonOperator = - | EndsWith of string - | StartsWith of string - | Contains of string + | EndsWith of FieldName : string * Comparer : StringComparer + | StartsWith of FieldName : string * Comparer : StringComparer + | Contains of FieldName : string * Comparer : StringComparer + | StringEquals of FieldName : string * Comparer : StringComparer | Equals of string | GreaterThan of string | GreaterThanOrEqual of string @@ -19,26 +20,40 @@ type private ComparisonOperator = | LessThanOrEqual of string | In of string + let rec private coerceObjectListFilterInput (variables : Variables) inputValue : Result = let parseFieldCondition (s : string) = - let s = s.ToLowerInvariant () let prefix (suffix : string) (s : string) = s.Substring (0, s.Length - suffix.Length) + // Phase 1: case-sensitive string ops – match original string against capitalized/uppercase suffixes + match s with + | s when s.EndsWith FilterSuffixConstants.CS.EndsWithSuffix && s.Length > FilterSuffixConstants.CS.EndsWithSuffix.Length -> EndsWith (prefix FilterSuffixConstants.CS.EndsWithSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.EWSuffix && s.Length > FilterSuffixConstants.CS.EWSuffix.Length -> EndsWith (prefix FilterSuffixConstants.CS.EWSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.StartsWithSuffix && s.Length > FilterSuffixConstants.CS.StartsWithSuffix.Length -> StartsWith (prefix FilterSuffixConstants.CS.StartsWithSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.SWSuffix && s.Length > FilterSuffixConstants.CS.SWSuffix.Length -> StartsWith (prefix FilterSuffixConstants.CS.SWSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.ContainsSuffix && s.Length > FilterSuffixConstants.CS.ContainsSuffix.Length -> Contains (prefix FilterSuffixConstants.CS.ContainsSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.EqualsSuffix && s.Length > FilterSuffixConstants.CS.EqualsSuffix.Length -> StringEquals (prefix FilterSuffixConstants.CS.EqualsSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.EQSuffix && s.Length > FilterSuffixConstants.CS.EQSuffix.Length -> StringEquals (prefix FilterSuffixConstants.CS.EQSuffix s, StringComparer.CurrentCulture) + | _ -> + // Phase 2: case-insensitive string ops and numeric ops – lower-case before matching + let s = s.ToLowerInvariant () match s with - | s when s.EndsWith ("_ends_with") && s.Length > "_ends_with".Length -> EndsWith (prefix "_ends_with" s) - | s when s.EndsWith ("_ew") && s.Length > "_ew".Length -> EndsWith (prefix "_ew" s) - | s when s.EndsWith ("_starts_with") && s.Length > "_starts_with".Length -> StartsWith (prefix "_starts_with" s) - | s when s.EndsWith ("_sw") && s.Length > "_sw".Length -> StartsWith (prefix "_sw" s) - | s when s.EndsWith ("_contains") && s.Length > "_contains".Length -> Contains (prefix "_contains" s) - | s when s.EndsWith ("_greater_than") && s.Length > "_greater_than".Length -> GreaterThan (prefix "_greater_than" s) - | s when s.EndsWith ("_gt") && s.Length > "_gt".Length -> GreaterThan (prefix "_gt" s) - | s when s.EndsWith ("_greater_than_or_equal") && s.Length > "_greater_than_or_equal".Length -> GreaterThanOrEqual (prefix "_greater_than_or_equal" s) - | s when s.EndsWith ("_gte") && s.Length > "_gte".Length -> GreaterThanOrEqual (prefix "_gte" s) - | s when s.EndsWith ("_less_than") && s.Length > "_less_than".Length -> LessThan (prefix "_less_than" s) - | s when s.EndsWith ("_lt") && s.Length > "_lt".Length -> LessThan (prefix "_lt" s) - | s when s.EndsWith ("_less_than_or_equal") && s.Length > "_less_than_or_equal".Length -> LessThanOrEqual (prefix "_less_than_or_equal" s) - | s when s.EndsWith ("_lte") && s.Length > "_lte".Length -> LessThanOrEqual (prefix "_lte" s) - | s when s.EndsWith ("_in") && s.Length > "_in".Length -> In (prefix "_in" s) + | s when s.EndsWith FilterSuffixConstants.CI.EndsWithSuffix && s.Length > FilterSuffixConstants.CI.EndsWithSuffix.Length -> EndsWith (prefix FilterSuffixConstants.CI.EndsWithSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.EWSuffix && s.Length > FilterSuffixConstants.CI.EWSuffix.Length -> EndsWith (prefix FilterSuffixConstants.CI.EWSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.StartsWithSuffix && s.Length > FilterSuffixConstants.CI.StartsWithSuffix.Length -> StartsWith (prefix FilterSuffixConstants.CI.StartsWithSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.SWSuffix && s.Length > FilterSuffixConstants.CI.SWSuffix.Length -> StartsWith (prefix FilterSuffixConstants.CI.SWSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.ContainsSuffix && s.Length > FilterSuffixConstants.CI.ContainsSuffix.Length -> Contains (prefix FilterSuffixConstants.CI.ContainsSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.EqualsSuffix && s.Length > FilterSuffixConstants.CI.EqualsSuffix.Length -> StringEquals (prefix FilterSuffixConstants.CI.EqualsSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.EQSuffix && s.Length > FilterSuffixConstants.CI.EQSuffix.Length -> StringEquals (prefix FilterSuffixConstants.CI.EQSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.GreaterThanOrEqualSuffix && s.Length > FilterSuffixConstants.GreaterThanOrEqualSuffix.Length -> GreaterThanOrEqual (prefix FilterSuffixConstants.GreaterThanOrEqualSuffix s) + | s when s.EndsWith FilterSuffixConstants.GTESuffix && s.Length > FilterSuffixConstants.GTESuffix.Length -> GreaterThanOrEqual (prefix FilterSuffixConstants.GTESuffix s) + | s when s.EndsWith FilterSuffixConstants.GreaterThanSuffix && s.Length > FilterSuffixConstants.GreaterThanSuffix.Length -> GreaterThan (prefix FilterSuffixConstants.GreaterThanSuffix s) + | s when s.EndsWith FilterSuffixConstants.GTSuffix && s.Length > FilterSuffixConstants.GTSuffix.Length -> GreaterThan (prefix FilterSuffixConstants.GTSuffix s) + | s when s.EndsWith FilterSuffixConstants.LessThanOrEqualSuffix && s.Length > FilterSuffixConstants.LessThanOrEqualSuffix.Length -> LessThanOrEqual (prefix FilterSuffixConstants.LessThanOrEqualSuffix s) + | s when s.EndsWith FilterSuffixConstants.LTESuffix && s.Length > FilterSuffixConstants.LTESuffix.Length -> LessThanOrEqual (prefix FilterSuffixConstants.LTESuffix s) + | s when s.EndsWith FilterSuffixConstants.LessThanSuffix && s.Length > FilterSuffixConstants.LessThanSuffix.Length -> LessThan (prefix FilterSuffixConstants.LessThanSuffix s) + | s when s.EndsWith FilterSuffixConstants.LTSuffix && s.Length > FilterSuffixConstants.LTSuffix.Length -> LessThan (prefix FilterSuffixConstants.LTSuffix s) + | s when s.EndsWith FilterSuffixConstants.InSuffix && s.Length > FilterSuffixConstants.InSuffix.Length -> In (prefix FilterSuffixConstants.InSuffix s) | s -> Equals s let (|EquatableValue|NonEquatableValue|) v = @@ -96,15 +111,16 @@ let rec private coerceObjectListFilterInput (variables : Variables) inputValue : | Error errs -> Error errs | Ok ValueNone -> Ok ValueNone | Ok (ValueSome filter) -> Ok (ValueSome (Not filter)) - | EndsWith fname, StringValue value -> Ok (ValueSome (ObjectListFilter.EndsWith { FieldName = fname; Value = value })) - | StartsWith fname, StringValue value -> Ok (ValueSome (ObjectListFilter.StartsWith { FieldName = fname; Value = value })) - | Contains fname, ComparableValue value -> Ok (ValueSome (ObjectListFilter.Contains { FieldName = fname; Value = value })) + | EndsWith (fname, comparer), StringValue value -> Ok (ValueSome (ObjectListFilter.EndsWith ({ FieldName = fname; Value = value }, comparer))) + | StartsWith (fname, comparer), StringValue value -> Ok (ValueSome (ObjectListFilter.StartsWith ({ FieldName = fname; Value = value }, comparer))) + | Contains (fname, comparer), ComparableValue value -> Ok (ValueSome (ObjectListFilter.Contains ({ FieldName = fname; Value = value }, comparer))) + | StringEquals (fname, comparer), StringValue value -> Ok (ValueSome (ObjectListFilter.Equals ({ FieldName = fname; Value = value }, comparer))) | Equals fname, ObjectValue value -> match mapInput value with | Error errs -> Error errs | Ok ValueNone -> Ok ValueNone | Ok (ValueSome filter) -> Ok (ValueSome (FilterField { FieldName = fname; Value = filter })) - | Equals fname, EquatableValue value -> Ok (ValueSome (ObjectListFilter.Equals { FieldName = fname; Value = value })) + | Equals fname, EquatableValue value -> Ok (ValueSome (ObjectListFilter.Equals ({ FieldName = fname; Value = value }, null))) | GreaterThan fname, ComparableValue value -> Ok (ValueSome (ObjectListFilter.GreaterThan { FieldName = fname; Value = value })) | GreaterThanOrEqual fname, ComparableValue value -> Ok (ValueSome (ObjectListFilter.GreaterThanOrEqual { FieldName = fname; Value = value })) | LessThan fname, ComparableValue value -> Ok (ValueSome (ObjectListFilter.LessThan { FieldName = fname; Value = value })) @@ -165,7 +181,14 @@ let ObjectListFilterType : InputCustomDefinition = { Name = "ObjectListFilter" Description = Some - "The `Filter` scalar type represents a filter on one or more fields of an object in an object list. The filter is represented by a JSON object where the fields are the complemented by specific suffixes to represent a query." + (String.concat + " " + [ + "The ObjectListFilter value represents field filters for object lists." + "Lowercase string suffixes such as `_starts_with`/`_sw`, `_ends_with`/`_ew`, `_contains` (no shorthand), and `_equals`/`_eq` are case-insensitive when applied to string fields." + "Capitalized string suffixes such as `_Starts_With`/`_SW`, `_Ends_With`/`_EW`, `_Contains` (no shorthand), and `_Equals`/`_EQ` are case-sensitive when applied to string fields." + "Comparison suffixes such as `_gt`, `_gte`, `_lt`, `_lte`, and `_in` are also supported." + ]) CoerceInput = (fun _ input variables -> match input with diff --git a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs index 8859057e8..0e177cae8 100644 --- a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs @@ -599,7 +599,7 @@ let ``Object list filter: must return filter information in Metadata`` () = ] ] let expectedFilter : KeyValuePair = - kvp ([ "A"; "s" ]) (And (Equals { FieldName = "id"; Value = 2L }, StartsWith { FieldName = "value"; Value = "A" })) + kvp ([ "A"; "s" ]) (And (Equals ({ FieldName = "id"; Value = 2L }, null), StartsWith ({ FieldName = "value"; Value = "A" }, StringComparer.CurrentCultureIgnoreCase))) let result = execute query ensureDirect result <| fun data errors -> @@ -647,7 +647,7 @@ let ``Object list filter: Must return AND filter information in Metadata`` () = ] ] let expectedFilter : KeyValuePair = - kvp ([ "A"; "subjects" ]) (And (StartsWith { FieldName = "value"; Value = "3" }, Equals { FieldName = "id"; Value = 6L })) + kvp ([ "A"; "subjects" ]) (And (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase), Equals ({ FieldName = "id"; Value = 6L }, null))) let result = execute query ensureDirect result <| fun data errors -> @@ -693,7 +693,7 @@ let ``Object list filter: Must return OR filter information in Metadata`` () = ] ] let expectedFilter : KeyValuePair = - kvp ([ "A"; "subjects" ]) (Or (StartsWith { FieldName = "value"; Value = "3" }, Equals { FieldName = "id"; Value = 6L })) + kvp ([ "A"; "subjects" ]) (Or (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase), Equals ({ FieldName = "id"; Value = 6L }, null))) let result = execute query ensureDirect result <| fun data errors -> @@ -785,7 +785,7 @@ let ``Object list filter: Must return Contains filter information in Metadata`` ] ] let expectedFilter : KeyValuePair = - kvp ([ "A"; "subjects" ]) (Contains { FieldName = "value"; Value = "3" }) + kvp ([ "A"; "subjects" ]) (Contains ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase)) let result = execute query ensureDirect result <| fun data errors -> @@ -831,7 +831,7 @@ let ``Object list filter: Must return NOT filter information in Metadata`` () = ] ] let expectedFilter : KeyValuePair = - kvp ([ "A"; "subjects" ]) (Not (StartsWith { FieldName = "value"; Value = "3" })) + kvp ([ "A"; "subjects" ]) (Not (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase))) let result = execute query ensureDirect result <| fun data errors -> @@ -879,7 +879,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notStartsFilter = """{ "not": { "value_starts_with": "3" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notStartsFilter) - let filter = Not (StartsWith { FieldName = "value"; Value = "3" }) + let filter = Not (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -891,7 +891,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notEndsFilter = """{ "not": { "value_ends_with": "2" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notEndsFilter) - let filter = Not (EndsWith { FieldName = "value"; Value = "2" }) + let filter = Not (EndsWith ({ FieldName = "value"; Value = "2" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -903,7 +903,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notStartsFilter = """{ "not": { "value_sw": "3" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notStartsFilter) - let filter = Not (StartsWith { FieldName = "value"; Value = "3" }) + let filter = Not (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -915,7 +915,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notEndsFilter = """{ "not": { "value_ew": "2" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notEndsFilter) - let filter = Not (EndsWith { FieldName = "value"; Value = "2" }) + let filter = Not (EndsWith ({ FieldName = "value"; Value = "2" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -1023,7 +1023,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notContainsFilter = """{ "not": { "value_contains": "A" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notContainsFilter) - let filter = Not (Contains { FieldName = "value"; Value = "A" }) + let filter = Not (Contains ({ FieldName = "value"; Value = "A" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -1035,7 +1035,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notEqualsFilter = """{ "not": { "value": "A2" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notEqualsFilter) - let filter = Not (Equals { FieldName = "value"; Value = "A2" }) + let filter = Not (Equals ({ FieldName = "value"; Value = "A2" }, null)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -1084,7 +1084,7 @@ let ``Object list filter: Must parse filter that references variables`` () = do let filterValue = "3" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", filterValue) - let filter = (StartsWith { FieldName = "value"; Value = "3" }) + let filter = (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs index d249e7883..c8a392009 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs @@ -103,7 +103,7 @@ let filterOptions = ObjectListFilterLinqOptions.None [] let ``ObjectListFilter works with Equals operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "validStringStruct"; Value = "Jonathan" } + let filter = Equals ({ FieldName = "validStringStruct"; Value = "Jonathan" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["validStringStruct"] = "Jonathan")""" @@ -111,7 +111,7 @@ let ``ObjectListFilter works with Equals operator for ValidStringStruct`` () = [] let ``ObjectListFilter works with not Equals operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = Not (Equals { FieldName = "validStringStruct"; Value = "Jonathan" }) + let filter = Not (Equals ({ FieldName = "validStringStruct"; Value = "Jonathan" }, null)) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["validStringStruct"] != "Jonathan")""" @@ -119,7 +119,7 @@ let ``ObjectListFilter works with not Equals operator for ValidStringStruct`` () [] let ``ObjectListFilter works with Equals operator for ValueOptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "valueOptionString"; Value = "Jonathan" } + let filter = Equals ({ FieldName = "valueOptionString"; Value = "Jonathan" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["valueOptionString"] = "Jonathan")""" @@ -127,7 +127,7 @@ let ``ObjectListFilter works with Equals operator for ValueOptionString`` () = [] let ``ObjectListFilter works with not Equals operator for ValueOptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Not (Equals { FieldName = "valueOptionString"; Value = "Jonathan" }) + let filter = Not (Equals ({ FieldName = "valueOptionString"; Value = "Jonathan" }, null)) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["valueOptionString"] != "Jonathan")""" @@ -135,7 +135,7 @@ let ``ObjectListFilter works with not Equals operator for ValueOptionString`` () [] let ``ObjectListFilter works with Equals operator for null ValueOptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "valueOptionString"; Value = null } + let filter = Equals ({ FieldName = "valueOptionString"; Value = null }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionString"] = null)""" @@ -143,7 +143,7 @@ let ``ObjectListFilter works with Equals operator for null ValueOptionString`` ( [] let ``ObjectListFilter works with Equals operator for ValueNone ValueOptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "valueOptionString"; Value = (ValueNone : voption) } + let filter = Equals ({ FieldName = "valueOptionString"; Value = (ValueNone : voption) }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionString"] = null)""" @@ -151,7 +151,7 @@ let ``ObjectListFilter works with Equals operator for ValueNone ValueOptionStrin [] let ``ObjectListFilter works with not Equals operator for ValueNone ValueOptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Not (Equals { FieldName = "valueOptionString"; Value = (ValueNone : voption) }) + let filter = Not (Equals ({ FieldName = "valueOptionString"; Value = (ValueNone : voption) }, null)) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionString"] != null)""" @@ -159,7 +159,7 @@ let ``ObjectListFilter works with not Equals operator for ValueNone ValueOptionS [] let ``ObjectListFilter works with Equals operator for OptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "optionString"; Value = "Jonathan" } + let filter = Equals ({ FieldName = "optionString"; Value = "Jonathan" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["optionString"] = "Jonathan")""" @@ -167,7 +167,7 @@ let ``ObjectListFilter works with Equals operator for OptionString`` () = [] let ``ObjectListFilter works with not Equals operator for OptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Not (Equals { FieldName = "optionString"; Value = "Jonathan" }) + let filter = Not (Equals ({ FieldName = "optionString"; Value = "Jonathan" }, null)) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["optionString"] != "Jonathan")""" @@ -175,7 +175,7 @@ let ``ObjectListFilter works with not Equals operator for OptionString`` () = [] let ``ObjectListFilter works with Equals operator for null OptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "optionString"; Value = null } + let filter = Equals ({ FieldName = "optionString"; Value = null }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["optionString"] = null)""" @@ -183,7 +183,7 @@ let ``ObjectListFilter works with Equals operator for null OptionString`` () = [] let ``ObjectListFilter works with not Equals operator for null OptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Not (Equals { FieldName = "optionString"; Value = null }) + let filter = Not (Equals ({ FieldName = "optionString"; Value = null }, null)) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["optionString"] = null)""" @@ -191,31 +191,55 @@ let ``ObjectListFilter works with not Equals operator for null OptionString`` () [] let ``ObjectListFilter works with StartsWith operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = StartsWith { FieldName = "validStringStruct"; Value = "J" } + let filter = StartsWith ({ FieldName = "validStringStruct"; Value = "J" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE STARTSWITH(root["validStringStruct"], "J")""" +[] +let ``ObjectListFilter works with StartsWith case insensitive operator for ValidStringStruct`` () = + let queryable = container.GetItemLinqQueryable () + let filter = StartsWith ({ FieldName = "validStringStruct"; Value = "J" }, StringComparer.OrdinalIgnoreCase) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE STARTSWITH(root["validStringStruct"], "J", true)""" + [] let ``ObjectListFilter works with EndsWith operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = EndsWith { FieldName = "validStringStruct"; Value = "n" } + let filter = EndsWith ({ FieldName = "validStringStruct"; Value = "n" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ENDSWITH(root["validStringStruct"], "n")""" +[] +let ``ObjectListFilter works with EndsWith case insensitive operator for ValidStringStruct`` () = + let queryable = container.GetItemLinqQueryable () + let filter = EndsWith ({ FieldName = "validStringStruct"; Value = "n" }, StringComparer.OrdinalIgnoreCase) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ENDSWITH(root["validStringStruct"], "n", true)""" + [] let ``ObjectListFilter works with Contains operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = Contains { FieldName = "validStringStruct"; Value = "athan" } + let filter = Contains ({ FieldName = "validStringStruct"; Value = "athan" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE CONTAINS(root["validStringStruct"], "athan")""" +[] +let ``ObjectListFilter works with Contains case insensitive operator for ValidStringStruct`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Contains ({ FieldName = "validStringStruct"; Value = "athan" }, StringComparer.OrdinalIgnoreCase) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE CONTAINS(root["validStringStruct"], "athan", true)""" + [] let ``ObjectListFilter works with Contains operator for ValidStringStruct list`` () = let queryable = container.GetItemLinqQueryable () - let filter = Contains { FieldName = "validStringStructList"; Value = "athan" } + let filter = Contains ({ FieldName = "validStringStructList"; Value = "athan" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ARRAY_CONTAINS(root["validStringStructList"], "athan")""" @@ -238,7 +262,7 @@ let ``ObjectListFilter works with In operator for empty ValidStringStruct list`` [] let ``ObjectListFilter works with Equals operator for ValidStringObject`` () = - let filter = Equals { FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" } + let filter = Equals ({ FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" }, null) let queryable = container.GetItemLinqQueryable () let filterQuery = queryable.Apply (filter) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery @@ -246,7 +270,7 @@ let ``ObjectListFilter works with Equals operator for ValidStringObject`` () = [] let ``ObjectListFilter works with not Equals operator for ValidStringObject`` () = - let filter = Not (Equals { FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" }) + let filter = Not (Equals ({ FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" }, null)) let queryable = container.GetItemLinqQueryable () let filterQuery = queryable.Apply (filter) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs index f1c06fa8d..bdd14b703 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs @@ -9,7 +9,7 @@ open FSharp.Data.GraphQL.Tests.LinqTests [] let ``ObjectListFilter works with Equals operator`` () = - let filter = Equals { FieldName = "firstName"; Value = "Jonathan" } // :> IComparable + let filter = Equals ({ FieldName = "firstName"; Value = "Jonathan" }, null) // :> IComparable let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 1 @@ -90,7 +90,7 @@ let ``ObjectListFilter works with LessThanOrEqual operator`` () = [] let ``ObjectListFilter works with StartsWith operator`` () = - let filter = StartsWith { FieldName = "firstName"; Value = "J" } + let filter = StartsWith ({ FieldName = "firstName"; Value = "J" }, null) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -103,7 +103,7 @@ let ``ObjectListFilter works with StartsWith operator`` () = [] let ``ObjectListFilter works with Contains operator`` () = - let filter = Contains { FieldName = "firstName"; Value = "en" } + let filter = Contains ({ FieldName = "firstName"; Value = "en" }, null) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -116,7 +116,7 @@ let ``ObjectListFilter works with Contains operator`` () = [] let ``ObjectListFilter works with EndsWith operator`` () = - let filter = EndsWith { FieldName = "lastName"; Value = "ams" } + let filter = EndsWith ({ FieldName = "lastName"; Value = "ams" }, null) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -130,7 +130,7 @@ let ``ObjectListFilter works with EndsWith operator`` () = [] let ``ObjectListFilter works with AND operator`` () = let filter = - And (Contains { FieldName = "firstName"; Value = "en" }, Equals { FieldName = "lastName"; Value = "Adams" }) + And (Contains ({ FieldName = "firstName"; Value = "en" }, null), Equals ({ FieldName = "lastName"; Value = "Adams" }, null)) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 1 @@ -144,7 +144,7 @@ let ``ObjectListFilter works with AND operator`` () = [] let ``ObjectListFilter works with OR operator`` () = let filter = - Or (GreaterThan { FieldName = "id"; Value = 4 }, Equals { FieldName = "lastName"; Value = "Adams" }) + Or (GreaterThan { FieldName = "id"; Value = 4 }, Equals ({ FieldName = "lastName"; Value = "Adams" }, null)) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -191,7 +191,7 @@ let ``ObjectListFilter works with IN operator for int type field`` () = [] let ``ObjectListFilter works with Contains operator for array type field`` () = - let filter = Contains { FieldName = "friends"; Value = { Email = "j.abrams@gmail.com" } } + let filter = Contains ({ FieldName = "friends"; Value = { Email = "j.abrams@gmail.com" } }, null) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -215,7 +215,7 @@ let ``ObjectListFilter works with FilterField operator`` () = let filter = FilterField { FieldName = "Contact" - Value = Contains { FieldName = "Email"; Value = "j.trif@gmail.com" } + Value = Contains ({ FieldName = "Email"; Value = "j.trif@gmail.com" }, null) } let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList @@ -229,7 +229,7 @@ let ``ObjectListFilter works with FilterField operator`` () = [] let ``ObjectListFilter works with NOT operator`` () = - let filter = Not (Equals { FieldName = "lastName"; Value = "Adams" }) + let filter = Not (Equals ({ FieldName = "lastName"; Value = "Adams" }, null)) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -416,7 +416,7 @@ let ``ObjectListFilter works with getDiscriminatorValue for Horse`` () = [] let ``ObjectListFilter works with getDiscriminatorValue startsWith for Horse and Hamster`` () = let queryable = animalData.AsQueryable () - let filter = StartsWith { FieldName = "Discriminator"; Value = "H" } + let filter = StartsWith ({ FieldName = "Discriminator"; Value = "H" }, null) let options = ObjectListFilterLinqOptions ( (fun entity (discriminator : string) -> entity.Discriminator.StartsWith discriminator), @@ -449,7 +449,7 @@ let ``ObjectListFilter works with Contains operator on list collection propertie { Name = "Product D"; Tags = [ "Tag4"; "Tag5" ] } ] let queryable = productList.AsQueryable () - let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filter = Contains ({ FieldName = "Tags"; Value = "Tag3" }, null) let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 do @@ -471,7 +471,7 @@ let ``ObjectListFilter works with Contains operator on array collection properti { Name = "Product D"; Tags = [| "Tag4"; "Tag5" |] } ] let queryable = productArray.AsQueryable () - let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filter = Contains ({ FieldName = "Tags"; Value = "Tag3" }, null) let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 do @@ -493,7 +493,7 @@ let ``ObjectListFilter works with Contains operator on set collection properties { Name = "Product D"; Tags = [| "Tag4"; "Tag5" |] |> Set.ofArray } ] let queryable = productArray.AsQueryable () - let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filter = Contains ({ FieldName = "Tags"; Value = "Tag3" }, null) let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 do @@ -526,3 +526,95 @@ let ``ObjectListFilter OfTypes works with two or more types`` () = let animal = List.last filteredData animal.ID |> equals 4 animal.Name |> equals "Horse D" + +[] +let ``ObjectListFilter works with Equals case insensitive operator`` () = + let filter = Equals ({ FieldName = "firstName"; Value = "jonathan" }, StringComparer.OrdinalIgnoreCase) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + result.LastName |> equals "Abrams" + +[] +let ``ObjectListFilter works with Equals case sensitive operator`` () = + let filter = Equals ({ FieldName = "firstName"; Value = "jonathan" }, null) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0 + +[] +let ``ObjectListFilter works with Equals case insensitive operator upper case`` () = + let filter = Equals ({ FieldName = "firstName"; Value = "JONATHAN" }, StringComparer.OrdinalIgnoreCase) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + +[] +let ``ObjectListFilter works with Equals case sensitive operator upper case`` () = + let filter = Equals ({ FieldName = "firstName"; Value = "JONATHAN" }, null) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0 + +[] +let ``ObjectListFilter works with StartsWith case insensitive operator`` () = + let filter = StartsWith ({ FieldName = "firstName"; Value = "j" }, StringComparer.OrdinalIgnoreCase) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + +[] +let ``ObjectListFilter works with StartsWith case sensitive operator`` () = + let filter = StartsWith ({ FieldName = "firstName"; Value = "j" }, null) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0 + +[] +let ``ObjectListFilter works with EndsWith case insensitive operator`` () = + let filter = EndsWith ({ FieldName = "lastName"; Value = "AMS" }, StringComparer.OrdinalIgnoreCase) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.LastName |> equals "Adams" + let result = List.last filteredData + result.ID |> equals 2 + result.LastName |> equals "Abrams" + +[] +let ``ObjectListFilter works with EndsWith case sensitive operator`` () = + let filter = EndsWith ({ FieldName = "lastName"; Value = "AMS" }, null) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0 + +[] +let ``ObjectListFilter works with Contains case insensitive operator`` () = + let filter = Contains ({ FieldName = "firstName"; Value = "EN" }, StringComparer.OrdinalIgnoreCase) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + let result = List.last filteredData + result.ID |> equals 7 + result.FirstName |> equals "Jeneffer" + +[] +let ``ObjectListFilter works with Contains case sensitive operator`` () = + let filter = Contains ({ FieldName = "firstName"; Value = "EN" }, null) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0 From 2d94c67e143e06808f571c120ca36a5e06bf5271 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 9 Jul 2026 20:34:10 +0200 Subject: [PATCH 13/32] `ObjectListFilter` filter values to target type coercion (#589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactored `ObjectListFilter`: modularized, added type coercion - Moved filter operators and LINQ logic to ObjectListFilterModule.fs - Added `TypeCoercion.fs` for automatic filter value coercion (`Guid`, `DateTime`, F# DUs, etc.) - Introduced `FilterValueCoercer` and extended `ObjectListFilterLinqOptions` for custom coercion - Centralized filter suffix constants in `FilterSuffixConstants.fs` - Updated `SchemaDefinitions.fs` to use new suffix constants - Added `vtryFind` and `vtryPick` utilities for arrays/lists in `Extensions.fs` - Improved code style, documentation, and function signatures * Refactor 'ObjectListFilter' to use 'System.Text.Json' coercion Replaces custom value coercers with 'System.Text.Json'-based coercion in 'ObjectListFilter', supporting advanced scenarios like F# DUs and CLR enums via 'JsonSerializerOptions'. Updates 'ObjectListFilterLinqOptions' to accept 'JsonSerializerOptions'. Refactors 'TypeCoercion' module to use JSON serialization/deserialization for all type conversions. Updates filter application logic and expands the test suite with new files to cover a wide range of coercion scenarios. Updates documentation and usage examples accordingly. * Updateв schema, add type coercion guide, bug report, tools * Added bug report for InputObject array type mismatch with analysis and test cases * Added type coercion guide for ObjectListFilter with usage and API docs * Introduced format-changed-files.ps1 to batch-format changed F# files via Fantomas * Updated schema snapshots for relay-style connections and new scalars * Refactored field_aliases.fsx for relay-style friends connection * Optimized TypeCoercion.fs to use Utf8JsonWriter for value coercion * Added prompt template for automated PR/issue description generation * Rebase fix * Update filters to use `CurrentCulture` string comparison Updated all string comparison operations in `ObjectListFilter` and filter parsing logic to use `StringComparer.CurrentCulture` or `StringComparer.CurrentCultureIgnoreCase` instead of `Ordinal`/`OrdinalIgnoreCase`. Adjusted related test expectations to match. This ensures string-based filters now respect the current culture's case rules. * Rebase fixes * Removed unnecessary `ObjectListFilterValidationException` * Added test traits * AI review fixes * Fix ObjectListFilter IN coercion behavior and add converter/no-converter tests --- ...harp.Data.GraphQL.Server.Middleware.fsproj | 10 +- .../ObjectListFilter.fs | 415 ++--------- .../ObjectListFilterModule.fs | 653 ++++++++++++++++++ .../SchemaDefinitions.fs | 3 +- .../TypeCoercion.fs | 259 +++++++ .../TypeSystemExtensions.fs | 2 - .../Helpers/Extensions.fs | 58 ++ .../FSharp.Data.GraphQL.Tests.fsproj | 10 +- .../ObjectListFilterComparerMappingTests.fs | 169 +++++ .../ObjectListFilterLinqGenerateTests.fs | 16 +- .../ObjectListFilterLinqTests.fs | 4 +- .../TypeCoercionFilterFieldEnumerableTests.fs | 414 +++++++++++ .../TypeCoercionFilterInOperatorTests.fs | 235 +++++++ ...TypeCoercionFilterOptionCollectionTests.fs | 106 +++ .../TypeCoercionFilterTests.fs | 231 +++++++ .../TypeCoercionTests.Common.fs | 136 ++++ .../TypeCoercionValueTests.fs | 210 ++++++ .../SelectLinqTests.fs | 1 + .../TestAttributes.fs | 18 + 19 files changed, 2577 insertions(+), 373 deletions(-) create mode 100644 src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilterModule.fs create mode 100644 src/FSharp.Data.GraphQL.Server.Middleware/TypeCoercion.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterComparerMappingTests.fs rename tests/FSharp.Data.GraphQL.Tests/{ => ObjectListFilter}/ObjectListFilterLinqGenerateTests.fs (96%) rename tests/FSharp.Data.GraphQL.Tests/{ => ObjectListFilter}/ObjectListFilterLinqTests.fs (99%) create mode 100644 tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterFieldEnumerableTests.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterInOperatorTests.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterOptionCollectionTests.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterTests.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionTests.Common.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionValueTests.fs diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj b/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj index 9eddcf46f..4838ed431 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj +++ b/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj @@ -14,14 +14,22 @@ true--> + + + <_Parameter1>FSharp.Data.GraphQL.Tests + + + - + + + diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index bb4f80e47..847165acb 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -2,7 +2,7 @@ namespace FSharp.Data.GraphQL.Server.Middleware open System open System.Collections -open FSharp.Data.GraphQL +open System.Text.Json /// A filter definition for a field value. type FieldFilter<'Val> = { FieldName : string; Value : 'Val } @@ -34,18 +34,27 @@ type ObjectListFilter = | OfTypes of Type list | FilterField of FieldFilter -open System.Linq open System.Linq.Expressions open System.Runtime.InteropServices -open System.Reflection -open System.Collections.Generic type private CompareDiscriminatorExpression<'T, 'D> = Expression> /// -/// Allows to specify discriminator comparison or discriminator getter -/// and a function that return discriminator value depending on entity type +/// Initializes a new instance of with optional +/// LINQ translation settings including discriminator handling and In-operator behavior. /// +/// Entity type. +/// Discriminator value type. +/// +/// Optional custom discriminator comparison expression. +/// +/// +/// Optional discriminator value resolver for OfTypes filtering. +/// +/// +/// Optional serializer settings used during filter value coercion. +/// + /// /// // discriminator custom condition /// let result () = @@ -72,378 +81,60 @@ type private CompareDiscriminatorExpression<'T, 'D> = Expression -[] type ObjectListFilterLinqOptions<'T, 'D> - ([] compareDiscriminator : CompareDiscriminatorExpression<'T, 'D> | null, [] getDiscriminatorValue : (Type -> 'D) | null) = - + ( + /// Optional custom discriminator comparison expression. + [] compareDiscriminator : CompareDiscriminatorExpression<'T, 'D> | null, + /// Optional discriminator value resolver used for OfTypes filtering. + [] getDiscriminatorValue : (Type -> 'D) | null, + /// Optional serializer settings used during filter value coercion. + [] jsonOptions : JsonSerializerOptions | null + ) = + + /// Gets the optional custom discriminator comparison expression. member _.CompareDiscriminator = compareDiscriminator |> ValueOption.ofObj + + /// Gets the optional discriminator value resolver. member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj - static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null) + /// Gets optional serializer settings used during filter coercion. + member _.JsonOptions = jsonOptions |> ValueOption.ofObj + /// Default options with all features disabled. + static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null, null) + + /// Creates a discriminator comparison expression from a discriminator selector. static member GetCompareDiscriminator (getDiscriminatorValue : Expression>) = let tParam = Expression.Parameter (typeof<'T>, "x") let dParam = Expression.Parameter (typeof<'D>, "d") let body = Expression.Equal (Expression.Invoke (getDiscriminatorValue, tParam), dParam) Expression.Lambda> (body, tParam, dParam) + /// Initializes options using a discriminator selector expression. new (getDiscriminator : Expression>) = - ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null) - new (compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null) - new (getDiscriminatorValue : Type -> 'D) = - ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator = null, getDiscriminatorValue = getDiscriminatorValue) - new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = - ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue) - -/// Contains tooling for working with ObjectListFilter. -module ObjectListFilter = - /// Contains operators for building and comparing ObjectListFilter values. - module Operators = - /// Creates a new ObjectListFilter representing an AND operation between two existing ones. - let ( &&& ) x y = And (x, y) - - /// Creates a new ObjectListFilter representing an OR operation between two existing ones. - let ( ||| ) x y = Or (x, y) - - /// Creates a new ObjectListFilter representing an EQUALS operation between two comparable values. - let ( === ) fname value = Equals ({ FieldName = fname; Value = value }, null) - - /// Creates a new ObjectListFilter representing a GREATER THAN operation of a comparable value. - let ( >>> ) fname value = GreaterThan { FieldName = fname; Value = value } - - /// Creates a new ObjectListFilter representing a GREATER THAN OR EQUAL operation of a comparable value. - let ( ==> ) fname value = GreaterThanOrEqual { FieldName = fname; Value = value } - - /// Creates a new ObjectListFilter representing a LESS THAN operation of a comparable value. - let ( <<< ) fname value = LessThan { FieldName = fname; Value = value } - - /// Creates a new ObjectListFilter representing a LESS THAN OR EQUAL operation of a comparable value. - let ( <== ) fname value = LessThanOrEqual { FieldName = fname; Value = value } - - /// Creates a new ObjectListFilter representing a STARTS WITH operation of a string value. - let ( =@@ ) fname value = StartsWith ({ FieldName = fname; Value = value }, null) - - /// Creates a new ObjectListFilter representing an ENDS WITH operation of a string value. - let ( @@= ) fname value = EndsWith ({ FieldName = fname; Value = value }, null) - - /// Creates a new ObjectListFilter representing a CONTAINS operation. - let ( @=@ ) fname value = Contains ({ FieldName = fname; Value = value }, null) - - /// Creates a new ObjectListFilter representing a IN operation. - let ( =~= ) fname value = In { FieldName = fname; Value = value } - - /// Creates a new ObjectListFilter representing a field sub comparison. - let ( --> ) fname filter = FilterField { FieldName = fname; Value = filter } - - /// Creates a new ObjectListFilter representing a NOT operation for the existing one. - let ( !!! ) filter = Not filter - - /// Creates a new ObjectListFilter representing a case-insensitive EQUALS operation on a string value. - let ( ===~ ) fname (value : string) = Equals ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) - - /// Creates a new ObjectListFilter representing a case-insensitive STARTS WITH operation on a string value. - let ( =@@~ ) fname (value : string) = StartsWith ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) - - /// Creates a new ObjectListFilter representing a case-insensitive ENDS WITH operation on a string value. - let ( @@=~ ) fname (value : string) = EndsWith ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) - - /// Creates a new ObjectListFilter representing a case-insensitive CONTAINS operation on a string value. - let ( @=@~ ) fname (value : string) = Contains ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) - - let private genericWhereMethod = - typeof.GetMethods () - |> Seq.where (fun m -> m.Name = "Where") - |> Seq.find (fun m -> - let parameters = m.GetParameters () - parameters.Length = 2 - && parameters[1].ParameterType.GetGenericTypeDefinition () = typedefof>>) + ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, null) - // Helper to create Where expression - let whereExpr<'T> (query : IQueryable<'T>) (param : ParameterExpression) predicate = - let whereMethod = genericWhereMethod.MakeGenericMethod ([| typeof<'T> |]) - Expression.Call (whereMethod, [| query.Expression; Expression.Lambda> (predicate, param) |]) + /// Initializes options using a custom discriminator comparison expression. + new (compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>) = + ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, null) - let private objectType = typeof - let private stringType = typeof - let private genericIEnumerableType = typedefof> - - let private stringComparisonType = typeof - let private StringStartsWithMethod = stringType.GetMethod ("StartsWith", [| stringType; stringComparisonType |]) - let private StringEndsWithMethod = stringType.GetMethod ("EndsWith", [| stringType; stringComparisonType |]) - let private StringContainsMethod = stringType.GetMethod ("Contains", [| stringType; stringComparisonType |]) - let private StringEqualsMethod = stringType.GetMethod ("Equals", [| stringType; stringComparisonType |]) - let private unwrapOptionMethod = - FSharp.Data.GraphQL.Helpers.moduleType.GetMethod (nameof Helpers.unwrap) - - let private getCollectionInstanceContainsMethod (memberType : Type) = - memberType - .GetMethods(BindingFlags.Instance ||| BindingFlags.Public) - .FirstOrDefault (fun m -> m.Name = "Contains" && m.GetParameters().Length = 1) - |> ValueOption.ofObj - - let private getEnumerableContainsMethod (itemType : Type) = - match - typeof - .GetMethods(BindingFlags.Static ||| BindingFlags.Public) - .FirstOrDefault (fun m -> m.Name = "Contains" && m.GetParameters().Length = 2) - with - | null -> raise (MissingMemberException "Static 'Contains' method with 2 parameters not found on 'Enumerable' class") - | containsGenericStaticMethod -> containsGenericStaticMethod.MakeGenericMethod ([| itemType |]) - - let private getEnumerableCastMethod (itemType : Type) = - match - typeof - .GetMethods(BindingFlags.Static ||| BindingFlags.Public) - .FirstOrDefault (fun m -> m.Name = "Cast" && m.GetParameters().Length = 1) - with - | null -> raise (MissingMemberException "Static 'Cast' method with 1 parameter not found on 'Enumerable' class") - | castGenericStaticMethod -> castGenericStaticMethod.MakeGenericMethod ([| itemType |]) - - let getField (param : ParameterExpression) fieldName = Expression.PropertyOrField (param, fieldName) - - let hasEqualityOperator (``type`` : Type) = - ``type``.GetMethods (BindingFlags.Public ||| BindingFlags.Static) - |> Seq.exists (fun m -> m.Name = " op_Equality") - - let hasInequalityOperator (``type`` : Type) = - ``type``.GetMethods (BindingFlags.Public ||| BindingFlags.Static) - |> Seq.exists (fun m -> m.Name = "op_Inequality") - - [] - type SourceExpression private (expression : Expression) = - new (parameter : ParameterExpression) = SourceExpression (parameter :> Expression) - new (``member`` : MemberExpression) = SourceExpression (``member`` :> Expression) - member _.Value = expression - static member op_Implicit (source : SourceExpression) = source.Value - static member op_Implicit (parameter : ParameterExpression) = SourceExpression (parameter :> Expression) - static member op_Implicit (``member`` : MemberExpression) = SourceExpression (``member`` :> Expression) - - let equalsMethod = - objectType - |> _.GetMethods(BindingFlags.Instance ||| BindingFlags.Public) - |> Seq.where (fun m -> m.Name = "Equals") - |> Seq.head - - let staticEqualsMethod = - objectType - |> _.GetMethods(BindingFlags.Static ||| BindingFlags.Public) - |> Seq.where (fun m -> m.Name = "Equals") - |> Seq.head - - /// Maps an IComparer to a StringComparison value. - /// Returns ValueNone only when the comparer is null or is not a recognized StringComparer. - let private comparerToStringComparison (comparer : IComparer) = - match comparer with - | null -> ValueNone - | :? StringComparer as sc -> - if obj.ReferenceEquals (sc, StringComparer.OrdinalIgnoreCase) then ValueSome StringComparison.OrdinalIgnoreCase - elif obj.ReferenceEquals (sc, StringComparer.InvariantCultureIgnoreCase) then ValueSome StringComparison.InvariantCultureIgnoreCase - elif obj.ReferenceEquals (sc, StringComparer.CurrentCultureIgnoreCase) then ValueSome StringComparison.CurrentCultureIgnoreCase - elif obj.ReferenceEquals (sc, StringComparer.Ordinal) then ValueSome StringComparison.Ordinal - elif obj.ReferenceEquals (sc, StringComparer.InvariantCulture) then ValueSome StringComparison.InvariantCulture - elif obj.ReferenceEquals (sc, StringComparer.CurrentCulture) then ValueSome StringComparison.CurrentCulture - else ValueNone - | _ -> ValueNone - - let rec buildFilterExpr isEnumerableQuery (param : SourceExpression) buildTypeDiscriminatorCheck filter : Expression = - - let build = buildFilterExpr isEnumerableQuery param buildTypeDiscriminatorCheck - - let (|NoCast|Enumerable|NonEnumerableCast|) value = - if obj.ReferenceEquals (value, null) then NoCast - else if isEnumerableQuery then Enumerable - else NonEnumerableCast (value.GetType ()) - - let unsafeConvertTo ``type`` ``member`` = Expression.Convert (Expression.Convert (``member``, objectType), ``type``) - - let normalizeStringMemberExpr (``member`` : MemberExpression) : Expression = - match ``member``.Type with - | t when t = stringType -> ``member`` - | _ when not isEnumerableQuery -> unsafeConvertTo stringType ``member`` - | _ when isEnumerableQuery -> Expression.Convert (Expression.Call (unwrapOptionMethod, ``member``), stringType) - | _ -> Expression.Convert (``member``, stringType) - - match filter with - | Not (Equals (f, comparer)) -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - match comparerToStringComparison comparer with - | ValueSome comparison -> - let value = Helpers.unwrap (box f.Value) :?> string - Expression.Not (Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsMethod, Expression.Constant (value, typeof), Expression.Constant comparison)) :> Expression - | ValueNone -> - let hasEqualityOperator = hasEqualityOperator ``member``.Type - match f.Value with - | NoCast when hasEqualityOperator -> Expression.NotEqual (``member``, Expression.Constant f.Value) - | NoCast - | NonEnumerableCast _ -> - Expression.NotEqual (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) - | Enumerable -> - let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) - Expression.Not (Expression.Call (``const``, equalsMethod, ``member``)) - | Not f -> f |> build |> Expression.Not :> Expression - | And (f1, f2) -> Expression.AndAlso (build f1, build f2) - | Or (f1, f2) -> Expression.OrElse (build f1, build f2) - | Equals (f, comparer) -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - match comparerToStringComparison comparer with - | ValueSome comparison -> - let value = Helpers.unwrap (box f.Value) :?> string - Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsMethod, Expression.Constant (value, typeof), Expression.Constant comparison) :> Expression - | ValueNone -> - let hasEqualityOperator = hasEqualityOperator ``member``.Type - match f.Value with - | NoCast when hasEqualityOperator -> Expression.Equal (``member``, Expression.Constant f.Value) - | NoCast - | NonEnumerableCast _ -> - Expression.Equal (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) - | Enumerable -> - let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) - Expression.Call (``const``, equalsMethod, ``member``) - | GreaterThan f -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - match f.Value with - | NoCast -> Expression.GreaterThan (``member``, Expression.Constant f.Value) - | Enumerable -> Expression.GreaterThan (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) - | NonEnumerableCast ``type`` -> Expression.GreaterThan ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) - | LessThan f -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - match f.Value with - | NoCast -> Expression.LessThan (``member``, Expression.Constant f.Value) - | Enumerable -> Expression.LessThan (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) - | NonEnumerableCast ``type`` -> Expression.LessThan ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) - | GreaterThanOrEqual f -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - match f.Value with - | NoCast -> Expression.GreaterThanOrEqual (``member``, Expression.Constant f.Value) - | Enumerable -> Expression.GreaterThanOrEqual (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) - | NonEnumerableCast ``type`` -> Expression.GreaterThanOrEqual ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) - | LessThanOrEqual f -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - match f.Value with - | NoCast -> Expression.LessThanOrEqual (``member``, Expression.Constant f.Value) - | Enumerable -> Expression.LessThanOrEqual (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) - | NonEnumerableCast ``type`` -> Expression.LessThanOrEqual ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) - | StartsWith (f, comparer) -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let comparison = comparerToStringComparison (comparer :> IComparer) |> ValueOption.defaultValue StringComparison.CurrentCulture - Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithMethod, Expression.Constant f.Value, Expression.Constant comparison) - | EndsWith (f, comparer) -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let comparison = comparerToStringComparison (comparer :> IComparer) |> ValueOption.defaultValue StringComparison.CurrentCulture - Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithMethod, Expression.Constant f.Value, Expression.Constant comparison) - - | Contains (f, comparer) -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let isEnumerable (memberType : Type) = - not (Type.(=) (memberType, stringType)) - && typeof.IsAssignableFrom (memberType) - && memberType.GetInterfaces().Any (fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") - let normalizedValue = Values.normalizeOptional ``member``.Type f.Value - let callContains memberType = - let itemType = - if ``member``.Type.IsArray then - ``member``.Type.GetElementType () - else - ``member``.Type.GetGenericArguments()[0] - let valueType = - match normalizedValue with - | null -> itemType - | value -> value.GetType () - let castedMember = - if itemType = valueType then - ``member`` :> Expression - elif isEnumerableQuery then - let castMethod = getEnumerableCastMethod valueType - Expression.Call (castMethod, ``member``) - else - let castedEnumerableType = genericIEnumerableType.MakeGenericType ([| valueType |]) - unsafeConvertTo castedEnumerableType ``member`` - match getCollectionInstanceContainsMethod memberType with - | ValueNone -> - let enumerableContains = getEnumerableContainsMethod valueType - Expression.Call (enumerableContains, castedMember, Expression.Constant (normalizedValue)) - | ValueSome instanceContainsMethod -> Expression.Call (castedMember, instanceContainsMethod, Expression.Constant (normalizedValue)) - match ``member``.Member with - | :? PropertyInfo as prop when prop.PropertyType |> isEnumerable -> callContains prop.PropertyType - | :? FieldInfo as field when field.FieldType |> isEnumerable -> callContains field.FieldType - | _ -> - let unwrappedValue = Helpers.unwrap f.Value - let comparison = comparerToStringComparison comparer |> ValueOption.defaultValue StringComparison.CurrentCulture - Expression.Call (normalizeStringMemberExpr ``member``, StringContainsMethod, Expression.Constant (unwrappedValue :?> string, typeof), Expression.Constant comparison) - | In f when not (f.Value.IsEmpty) -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let enumerableContains = getEnumerableContainsMethod objectType - Expression.Call (enumerableContains, (Expression.Constant f.Value), Expression.Convert (``member``, objectType)) - | In f -> Expression.Constant (false) - | OfTypes types -> - types - |> Seq.map (fun t -> buildTypeDiscriminatorCheck param t) - |> Seq.reduce (fun acc expr -> Expression.OrElse (acc, expr)) - | FilterField f -> - let paramExpr = Expression.PropertyOrField (param, f.FieldName) - buildFilterExpr isEnumerableQuery (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value - - type private CompareDiscriminatorExpressionVisitor<'T, 'D> - (compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>, param : SourceExpression, value : obj) = - inherit ExpressionVisitor () - override _.VisitParameter (node) = - if node = compareDiscriminator.Parameters.[0] then - param.Value - elif node = compareDiscriminator.Parameters.[1] then - Expression.Constant (value) :> Expression - else - node :> Expression - - let enumerableQueryType = typedefof> - - let apply (options : ObjectListFilterLinqOptions<'T, 'D>) (filter : ObjectListFilter) (query : IQueryable<'T>) = - let isEnumerableQuery = query.GetType().GetGenericTypeDefinition () = enumerableQueryType - // Helper for discriminator comparison - let buildTypeDiscriminatorCheck (param : SourceExpression) (t : Type) = - match options.CompareDiscriminator, options.GetDiscriminatorValue with - | ValueNone, ValueNone -> - Expression.Equal ( - // Default discriminator property - Expression.PropertyOrField (param, "__typename"), - // Default discriminator value - Expression.Constant (t.FullName) - ) - :> Expression - | ValueSome discExpr, ValueNone -> - // Replace parameters from the original expression with our new ones - let replacer = CompareDiscriminatorExpressionVisitor (discExpr, param, t.FullName) - replacer.Visit discExpr.Body - | ValueNone, ValueSome discValueFn -> - let discriminatorValue = discValueFn t - Expression.Equal ( - // Default discriminator property - Expression.PropertyOrField (param, "__typename"), - // Provided discriminator value gathered from type - Expression.Constant (discriminatorValue) - ) - :> Expression - | ValueSome discExpr, ValueSome discValueFn -> - let discriminatorValue = discValueFn t - // Replace parameters from the original expression with our new ones - let replacer = CompareDiscriminatorExpressionVisitor (discExpr, param, discriminatorValue) - replacer.Visit discExpr.Body - let queryExpr = - let param = Expression.Parameter (typeof<'T>, "x") - let body = buildFilterExpr isEnumerableQuery (SourceExpression param) buildTypeDiscriminatorCheck filter - whereExpr<'T> query param body - // Create and execute the final expression - query.Provider.CreateQuery<'T> (queryExpr) - -[] -module ObjectListFilterExtensions = + /// Initializes options using a discriminator value resolver. + new (getDiscriminatorValue : Type -> 'D) = + ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, null) - open ObjectListFilter + /// Initializes options using both discriminator selector and value resolver. + new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = + ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue, null) - type ObjectListFilter with + /// Initializes options using serializer settings for filter coercion. + new (jsonOptions : JsonSerializerOptions) = + ObjectListFilterLinqOptions<'T, 'D> (null, null, jsonOptions) - member inline filter.ApplyTo<'T, 'D> (query : IQueryable<'T>, [] options : ObjectListFilterLinqOptions<'T, 'D>) = - apply options filter query + /// Initializes options using discriminator selector and serializer settings. + new (getDiscriminator : Expression>, jsonOptions : JsonSerializerOptions) = + ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, jsonOptions) - type IQueryable<'T> with + /// Initializes options using discriminator comparison and serializer settings. + new (compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>, jsonOptions : JsonSerializerOptions) = + ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, jsonOptions) - member inline query.Apply (filter : ObjectListFilter, [] options : ObjectListFilterLinqOptions<'T, 'D>) = apply options filter query diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilterModule.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilterModule.fs new file mode 100644 index 000000000..11908b5f9 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilterModule.fs @@ -0,0 +1,653 @@ +namespace FSharp.Data.GraphQL.Server.Middleware + +open System +open System.Collections +open System.Collections.Concurrent +open System.Collections.Generic +open System.Linq +open System.Linq.Expressions +open System.Reflection +open System.Runtime.InteropServices +open FSharp.Data.GraphQL + +/// Contains tooling for working with ObjectListFilter. +[] +module ObjectListFilter = + /// Contains operators for building and comparing ObjectListFilter values. + module Operators = + /// Creates a new ObjectListFilter representing an AND operation between two existing ones. + let (&&&) x y = And (x, y) + + /// Creates a new ObjectListFilter representing an OR operation between two existing ones. + let (|||) x y = Or (x, y) + + /// Creates a new ObjectListFilter representing an EQUALS operation between two comparable values. + let (===) fname value = Equals ({ FieldName = fname; Value = value }, null) + + /// Creates a new ObjectListFilter representing a GREATER THAN operation of a comparable value. + let (>>>) fname value = GreaterThan { FieldName = fname; Value = value } + + /// Creates a new ObjectListFilter representing a GREATER THAN OR EQUAL operation of a comparable value. + let (==>) fname value = GreaterThanOrEqual { FieldName = fname; Value = value } + + /// Creates a new ObjectListFilter representing a LESS THAN operation of a comparable value. + let (<<<) fname value = LessThan { FieldName = fname; Value = value } + + /// Creates a new ObjectListFilter representing a LESS THAN OR EQUAL operation of a comparable value. + let (<==) fname value = LessThanOrEqual { FieldName = fname; Value = value } + + /// Creates a new ObjectListFilter representing a STARTS WITH operation of a string value. + let (=@@) fname value = StartsWith ({ FieldName = fname; Value = value }, null) + + /// Creates a new ObjectListFilter representing an ENDS WITH operation of a string value. + let (@@=) fname value = EndsWith ({ FieldName = fname; Value = value }, null) + + /// Creates a new ObjectListFilter representing a CONTAINS operation. + let (@=@) fname value = Contains ({ FieldName = fname; Value = value }, null) + + /// Creates a new ObjectListFilter representing a IN operation. + let (=~=) fname value = In { FieldName = fname; Value = value } + + /// Creates a new ObjectListFilter representing a field sub comparison. + let (-->) fname filter = FilterField { FieldName = fname; Value = filter } + + /// Creates a new ObjectListFilter representing a NOT operation for the existing one. + let (!!!) filter = Not filter + + /// Creates a new ObjectListFilter representing a case-insensitive EQUALS operation on a string value. + let (===~) fname (value : string) = + Equals ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + + /// Creates a new ObjectListFilter representing a case-insensitive STARTS WITH operation on a string value. + let (=@@~) fname (value : string) = + StartsWith ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + + /// Creates a new ObjectListFilter representing a case-insensitive ENDS WITH operation on a string value. + let (@@=~) fname (value : string) = + EndsWith ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + + /// Creates a new ObjectListFilter representing a case-insensitive CONTAINS operation on a string value. + let (@=@~) fname (value : string) = + Contains ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + + let private genericWhereMethod = + typeof.GetMethods () + |> Seq.where (fun m -> m.Name = "Where") + |> Seq.find (fun m -> + let parameters = m.GetParameters () + parameters.Length = 2 + && parameters[1].ParameterType.GetGenericTypeDefinition () = typedefof>>) + + // Helper to create Where expression + let whereExpr<'T> (query : IQueryable<'T>) (param : ParameterExpression) predicate = + let whereMethod = genericWhereMethod.MakeGenericMethod ([| typeof<'T> |]) + Expression.Call (whereMethod, [| query.Expression; Expression.Lambda> (predicate, param) |]) + + let private objectType = typeof + let private stringType = typeof + let private genericIEnumerableType = typedefof> + let private enumerableType = typeof + let private iEnumerableType = typeof + + let private stringComparisonType = typeof + let private StringStartsWithMethod = + stringType.GetMethod ("StartsWith", [| stringType; stringComparisonType |]) + let private StringEndsWithMethod = + stringType.GetMethod ("EndsWith", [| stringType; stringComparisonType |]) + let private StringContainsMethod = + stringType.GetMethod ("Contains", [| stringType; stringComparisonType |]) + let private StringEqualsMethod = stringType.GetMethod ("Equals", [| stringType; stringComparisonType |]) + let private unwrapOptionMethod = + FSharp.Data.GraphQL.Helpers.moduleType.GetMethod (nameof Helpers.unwrap) + + /// Cache for MemberInfo (PropertyInfo or FieldInfo) lookups to avoid repeated reflection. + let private memberInfoCache = System.Collections.Concurrent.ConcurrentDictionary<(Type * string), MemberInfo voption> () + + /// Checks if a type is the generic IEnumerable interface using structural comparison. + let private isGenericIEnumerable (t : Type) : bool = + t.IsGenericType && t.GetGenericTypeDefinition () = genericIEnumerableType + + /// Gets MemberInfo (PropertyInfo or FieldInfo) from cache, performing reflection if not cached. + /// Mirrors the behavior of Expression.PropertyOrField which checks properties first, then fields. + let private getCachedMemberInfo (entityType : Type) (stripSuffix : string) : MemberInfo voption = + let key = (entityType, stripSuffix) + memberInfoCache.GetOrAdd( + key, + Func<(Type * string), MemberInfo voption> (fun _ -> + // Try property first (matches Expression.PropertyOrField behavior) + match + entityType.GetProperty ( + stripSuffix, + BindingFlags.Public + ||| BindingFlags.Instance + ||| BindingFlags.IgnoreCase + ) + with + | null -> + // Fall back to field if property not found + match + entityType.GetField ( + stripSuffix, + BindingFlags.Public + ||| BindingFlags.Instance + ||| BindingFlags.IgnoreCase + ) + with + | null -> ValueNone + | f -> ValueSome (f :> MemberInfo) + | p -> ValueSome (p :> MemberInfo) + ) + ) + + let private getCollectionInstanceContainsMethod (memberType : Type) = + memberType + .GetMethods(BindingFlags.Instance ||| BindingFlags.Public) + .FirstOrDefault (fun m -> m.Name = "Contains" && m.GetParameters().Length = 1) + |> ValueOption.ofObj + + let private getEnumerableContainsMethod (itemType : Type) = + match + enumerableType + .GetMethods(BindingFlags.Static ||| BindingFlags.Public) + .FirstOrDefault (fun m -> m.Name = "Contains" && m.GetParameters().Length = 2) + with + | null -> raise (MissingMemberException "Static 'Contains' method with 2 parameters not found on 'Enumerable' class") + | containsGenericStaticMethod -> containsGenericStaticMethod.MakeGenericMethod ([| itemType |]) + + let private getEnumerableCastMethod (itemType : Type) = + match + enumerableType + .GetMethods(BindingFlags.Static ||| BindingFlags.Public) + .FirstOrDefault (fun m -> m.Name = "Cast" && m.GetParameters().Length = 1) + with + | null -> raise (MissingMemberException "Static 'Cast' method with 1 parameter not found on 'Enumerable' class") + | castGenericStaticMethod -> castGenericStaticMethod.MakeGenericMethod ([| itemType |]) + + let getField (param : ParameterExpression) fieldName = Expression.PropertyOrField (param, fieldName) + + let hasEqualityOperator (``type`` : Type) = + ``type``.GetMethods (BindingFlags.Public ||| BindingFlags.Static) + |> Seq.exists (fun m -> m.Name = " op_Equality") + + let hasInequalityOperator (``type`` : Type) = + ``type``.GetMethods (BindingFlags.Public ||| BindingFlags.Static) + |> Seq.exists (fun m -> m.Name = "op_Inequality") + + [] + type SourceExpression private (expression : Expression) = + new (parameter : ParameterExpression) = SourceExpression (parameter :> Expression) + new (``member`` : MemberExpression) = SourceExpression (``member`` :> Expression) + member _.Value = expression + static member op_Implicit (source : SourceExpression) = source.Value + static member op_Implicit (parameter : ParameterExpression) = SourceExpression (parameter :> Expression) + static member op_Implicit (``member`` : MemberExpression) = SourceExpression (``member`` :> Expression) + + let equalsMethod = + objectType + |> _.GetMethods(BindingFlags.Instance ||| BindingFlags.Public) + |> Seq.where (fun m -> m.Name = "Equals") + |> Seq.head + + let staticEqualsMethod = + objectType + |> _.GetMethods(BindingFlags.Static ||| BindingFlags.Public) + |> Seq.where (fun m -> m.Name = "Equals") + |> Seq.head + + /// Maps an IComparer to a StringComparison value. + /// Returns ValueNone for null or unsupported comparers. + let internal comparerToStringComparison (comparer : IComparer) = + match comparer with + | null -> ValueNone + | :? StringComparer as sc -> + let mutable isOrdinalIgnoreCase = false + + if StringComparer.IsWellKnownOrdinalComparer (sc, &isOrdinalIgnoreCase) then + if isOrdinalIgnoreCase then + ValueSome StringComparison.OrdinalIgnoreCase + else + ValueSome StringComparison.Ordinal + else + let mutable compareInfo = Unchecked.defaultof + let mutable compareOptions = Globalization.CompareOptions.None + + if StringComparer.IsWellKnownCultureAwareComparer (sc, &compareInfo, &compareOptions) then + let isInvariantCulture = compareInfo.Equals Globalization.CultureInfo.InvariantCulture.CompareInfo + let isCurrentCulture = compareInfo.Equals Globalization.CultureInfo.CurrentCulture.CompareInfo + + match compareOptions with + | Globalization.CompareOptions.None when isInvariantCulture -> ValueSome StringComparison.InvariantCulture + | Globalization.CompareOptions.IgnoreCase when isInvariantCulture -> + ValueSome StringComparison.InvariantCultureIgnoreCase + | Globalization.CompareOptions.None when isCurrentCulture -> ValueSome StringComparison.CurrentCulture + | Globalization.CompareOptions.IgnoreCase when isCurrentCulture -> + ValueSome StringComparison.CurrentCultureIgnoreCase + | _ -> ValueNone + else + ValueNone + | _ -> ValueNone + + /// Gets the type from a MemberInfo (PropertyInfo or FieldInfo). + let private getMemberType (member' : MemberInfo) : Type = + match member' with + | :? PropertyInfo as p -> p.PropertyType + | :? FieldInfo as f -> f.FieldType + | _ -> invalidOp $"Unsupported member type: {member'.GetType().Name}" + + /// Resolves the field type within a given entity, stripping suffixes and unwrapping options. + let private getFieldTypeForEntity (entityType : Type) (fieldName : string) : Type voption = + let stripSuffix = TypeCoercion.stripOperatorSuffix fieldName + match getCachedMemberInfo entityType stripSuffix with + | ValueNone -> ValueNone + | ValueSome member' -> ValueSome (TypeCoercion.unwrapOption (getMemberType member')) + + /// Returns both the original member type and the unwrapped type. + /// Useful for detecting if we need to unwrap option expressions at runtime. + let private getFieldTypeAndOriginal (entityType : Type) (fieldName : string) : (Type * Type) voption = + let stripSuffix = TypeCoercion.stripOperatorSuffix fieldName + match getCachedMemberInfo entityType stripSuffix with + | ValueNone -> ValueNone + | ValueSome ``member`` -> + let originalType = getMemberType ``member`` + let unwrappedType = TypeCoercion.unwrapOption originalType + ValueSome (originalType, unwrappedType) + + /// Detects if a type is enumerable (but not string). + let private isEnumerableType (``type`` : Type) : bool = + not (Type.(=) (``type``, stringType)) + && iEnumerableType.IsAssignableFrom (``type``) + && ``type``.GetInterfaces().Any (fun i -> isGenericIEnumerable i) + + /// Unwraps the element type from an enumerable type. + let private tryGetEnumerableElementType (``type`` : Type) : Type voption = TypeCoercion.tryUnwrapEnumerableElement ``type`` + + /// Gets the closed generic method for the given element type. + let private getEnumerableAnyMethod (elementType : Type) : MethodInfo = + match + enumerableType + .GetMethods(BindingFlags.Static ||| BindingFlags.Public) + .FirstOrDefault (fun m -> m.Name = "Any" && m.GetParameters().Length = 2) + with + | null -> + let message = + $"Static 'Any' method with 2 parameters not found on '{enumerableType.FullName}' class. Expected signature: Any(IEnumerable, Func). " + raise (MissingMemberException message) + | anyGenericStaticMethod -> anyGenericStaticMethod.MakeGenericMethod ([| elementType |]) + + let private normalizeInValue (fieldType : Type) (value : obj) : obj = + let normalized = Values.normalizeOptional fieldType value + if obj.ReferenceEquals (normalized, null) then + null + elif fieldType.IsGenericType && fieldType.GetGenericTypeDefinition () = typedefof> then + let underlyingType = Nullable.GetUnderlyingType fieldType + if not (obj.ReferenceEquals (underlyingType, null)) && normalized.GetType () = underlyingType then + Activator.CreateInstance (fieldType, normalized) + else + normalized + else + normalized + + let private materializeTypedInArray (fieldType : Type) (values : obj list) : Array = + let array = Array.CreateInstance (fieldType, values.Length) + values + |> List.iteri (fun index value -> array.SetValue (normalizeInValue fieldType value, index)) + array + + let rec buildFilterExpr isEnumerableQuery (param : SourceExpression) buildTypeDiscriminatorCheck filter : Expression = + + let build = buildFilterExpr isEnumerableQuery param buildTypeDiscriminatorCheck + + let (|NoCast|Enumerable|NonEnumerableCast|) value = + if obj.ReferenceEquals (value, null) then NoCast + else if isEnumerableQuery then Enumerable + else NonEnumerableCast (value.GetType ()) + + let unsafeConvertTo ``type`` ``member`` = Expression.Convert (Expression.Convert (``member``, objectType), ``type``) + + let normalizeStringMemberExpr (``member`` : Expression) : Expression = + let memberType = ``member``.Type + match memberType with + | t when t = stringType -> ``member`` + | _ when not isEnumerableQuery -> unsafeConvertTo stringType ``member`` + | _ when isEnumerableQuery -> + // For ParameterExpression (from "_"), we can't call unwrapOptionMethod directly + if ``member`` :? ParameterExpression then + Expression.Convert (``member``, stringType) + else + match ``member`` with + | :? MemberExpression as me -> Expression.Convert (Expression.Call (unwrapOptionMethod, me), stringType) + | _ -> Expression.Convert (``member``, stringType) + | _ -> Expression.Convert (``member``, stringType) + + match filter with + | Not (Equals (f, comparer)) -> + let ``member`` = + // Special case: "_" means the element itself, not a field property + if f.FieldName = "_" then + param.Value + else + Expression.PropertyOrField (param, f.FieldName) + let unwrappedMemberType = TypeCoercion.unwrapOption ``member``.Type + match comparerToStringComparison comparer with + | ValueSome comparison when Type.(=) (unwrappedMemberType, stringType) -> + let value = Helpers.unwrap (box f.Value) :?> string + Expression.Not ( + Expression.Call ( + normalizeStringMemberExpr ``member``, + StringEqualsMethod, + Expression.Constant (value, stringType), + Expression.Constant comparison + ) + ) + :> Expression + | ValueSome _ + | ValueNone -> + let hasEqualityOperator = hasEqualityOperator ``member``.Type + match f.Value with + | NoCast when hasEqualityOperator -> Expression.NotEqual (``member``, Expression.Constant f.Value) + | NoCast + | NonEnumerableCast _ -> + Expression.NotEqual (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) + | Enumerable -> + let normalized = Values.normalizeOptional ``member``.Type f.Value + let ``const`` = Expression.Constant (normalized) + let boxedArg = Expression.Convert (``member``, objectType) + Expression.Not (Expression.Call (``const``, equalsMethod, boxedArg)) + | Not f -> f |> build |> Expression.Not :> Expression + | And (f1, f2) -> Expression.AndAlso (build f1, build f2) + | Or (f1, f2) -> Expression.OrElse (build f1, build f2) + | Equals (f, comparer) -> + let ``member`` = + // Special case: "_" means the element itself, not a field property + if f.FieldName = "_" then + param.Value + else + Expression.PropertyOrField (param, f.FieldName) + let unwrappedMemberType = TypeCoercion.unwrapOption ``member``.Type + match comparerToStringComparison comparer with + | ValueSome comparison when Type.(=) (unwrappedMemberType, stringType) -> + let value = Helpers.unwrap (box f.Value) :?> string + Expression.Call ( + normalizeStringMemberExpr ``member``, + StringEqualsMethod, + Expression.Constant (value, stringType), + Expression.Constant comparison + ) + :> Expression + | ValueSome _ + | ValueNone -> + let hasEqualityOperator = hasEqualityOperator ``member``.Type + match f.Value with + | NoCast when hasEqualityOperator -> Expression.Equal (``member``, Expression.Constant f.Value) + | NoCast + | NonEnumerableCast _ -> + Expression.Equal (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) + | Enumerable -> + let normalized = Values.normalizeOptional ``member``.Type f.Value + let ``const`` = Expression.Constant (normalized) + let boxedArg = Expression.Convert (``member``, objectType) + Expression.Call (``const``, equalsMethod, boxedArg) + | GreaterThan f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + match f.Value with + | NoCast -> Expression.GreaterThan (``member``, Expression.Constant f.Value) + | Enumerable -> Expression.GreaterThan (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) + | NonEnumerableCast ``type`` -> Expression.GreaterThan ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) + | LessThan f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + match f.Value with + | NoCast -> Expression.LessThan (``member``, Expression.Constant f.Value) + | Enumerable -> Expression.LessThan (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) + | NonEnumerableCast ``type`` -> Expression.LessThan ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) + | GreaterThanOrEqual f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + match f.Value with + | NoCast -> Expression.GreaterThanOrEqual (``member``, Expression.Constant f.Value) + | Enumerable -> Expression.GreaterThanOrEqual (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) + | NonEnumerableCast ``type`` -> Expression.GreaterThanOrEqual ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) + | LessThanOrEqual f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + match f.Value with + | NoCast -> Expression.LessThanOrEqual (``member``, Expression.Constant f.Value) + | Enumerable -> Expression.LessThanOrEqual (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) + | NonEnumerableCast ``type`` -> Expression.LessThanOrEqual ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) + | StartsWith (f, comparer) -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + let comparison = + comparerToStringComparison comparer + |> ValueOption.defaultValue StringComparison.CurrentCulture + Expression.Call ( + normalizeStringMemberExpr ``member``, + StringStartsWithMethod, + Expression.Constant f.Value, + Expression.Constant comparison + ) + | EndsWith (f, comparer) -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + let comparison = + comparerToStringComparison comparer + |> ValueOption.defaultValue StringComparison.CurrentCulture + Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithMethod, Expression.Constant f.Value, Expression.Constant comparison) + + | Contains (f, comparer) -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + let isEnumerable (memberType : Type) = + not (Type.(=) (memberType, stringType)) + && iEnumerableType.IsAssignableFrom (memberType) + && memberType.GetInterfaces().Any (fun i -> isGenericIEnumerable i) + let normalizedValue = Values.normalizeOptional ``member``.Type f.Value + let callContains memberType = + let itemType = + if ``member``.Type.IsArray then + ``member``.Type.GetElementType () + else + ``member``.Type.GetGenericArguments()[0] + let valueType = + match normalizedValue with + | null -> itemType + | value -> value.GetType () + let castedMember = + if itemType = valueType then + ``member`` :> Expression + elif isEnumerableQuery then + let castMethod = getEnumerableCastMethod valueType + Expression.Call (castMethod, ``member``) + else + let castedEnumerableType = genericIEnumerableType.MakeGenericType ([| valueType |]) + unsafeConvertTo castedEnumerableType ``member`` + match getCollectionInstanceContainsMethod memberType with + | ValueNone -> + let enumerableContains = getEnumerableContainsMethod valueType + Expression.Call (enumerableContains, castedMember, Expression.Constant (normalizedValue)) + | ValueSome instanceContainsMethod -> Expression.Call (castedMember, instanceContainsMethod, Expression.Constant (normalizedValue)) + match ``member``.Member with + | :? PropertyInfo as prop when prop.PropertyType |> isEnumerable -> callContains prop.PropertyType + | :? FieldInfo as field when field.FieldType |> isEnumerable -> callContains field.FieldType + | _ -> + let unwrappedValue = Helpers.unwrap f.Value + let comparison = + comparerToStringComparison comparer + |> ValueOption.defaultValue StringComparison.CurrentCulture + Expression.Call ( + normalizeStringMemberExpr ``member``, + StringContainsMethod, + Expression.Constant (unwrappedValue :?> string, stringType), + Expression.Constant comparison + ) + | In f when not (f.Value.IsEmpty) -> + let ``member`` = + // Special case: "_" means the element itself, not a field property + if f.FieldName = "_" then + param.Value + else + Expression.PropertyOrField (param, f.FieldName) + let fieldType = ``member``.Type + let typedValues = materializeTypedInArray fieldType f.Value + let enumerableContains = getEnumerableContainsMethod fieldType + Expression.Call (enumerableContains, Expression.Constant typedValues, ``member``) + | In f -> Expression.Constant (false) + | OfTypes types -> + types + |> Seq.map (fun t -> buildTypeDiscriminatorCheck param t) + |> Seq.reduce (fun acc expr -> Expression.OrElse (acc, expr)) + | FilterField f -> + let paramType = param.Value.Type + match getFieldTypeAndOriginal paramType f.FieldName with + | ValueNone -> + // Fallback: just recurse (may fail downstream) + let paramExpr = Expression.PropertyOrField (param, f.FieldName) + buildFilterExpr isEnumerableQuery (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value + | ValueSome (originalFieldType, unwrappedFieldType) -> + // Check if the UNWRAPPED type is enumerable + let isCollection = isEnumerableType unwrappedFieldType + + // Check if this is an option-wrapped collection + let isOptionWrapped = not (Type.(=) (originalFieldType, unwrappedFieldType)) + + if isCollection then + let effectiveType = unwrappedFieldType + match tryGetEnumerableElementType effectiveType with + | ValueNone -> + // Should not happen for isEnumerableType, but fallback to direct traversal + let paramExpr = Expression.PropertyOrField (param, f.FieldName) + buildFilterExpr isEnumerableQuery (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value + | ValueSome elementType -> + // Create lambda parameter for element + let elemParam = Expression.Parameter (elementType, "x") + // Recursively build inner filter over element type + let innerExpr = buildFilterExpr false (SourceExpression elemParam) buildTypeDiscriminatorCheck f.Value + // Lambda: x => innerExpr + let funcGenericDef = typeof>.GetGenericTypeDefinition () + let lambdaType = funcGenericDef.MakeGenericType ([| elementType; typeof |]) + let lambda = Expression.Lambda (lambdaType, innerExpr, elemParam) :> Expression + + let rawCollExpr = Expression.PropertyOrField (param, f.FieldName) + + if isOptionWrapped then + // Option-wrapped collection: e.g., some_field : option> + // Strategy: Access the wrapped collection via .Value and pass it to Any with the predicate. + // If the option is None, accessing .Value throws NullReferenceException. + // We wrap the entire Any call in try-catch to safely return false for None, + // effectively treating None collections as "no match". + let anyMethod = getEnumerableAnyMethod elementType + let valueExpr = Expression.PropertyOrField (rawCollExpr, "Value") + let anyCall = Expression.Call (anyMethod, valueExpr, lambda) + + // Wrap in try-catch: try { Any(opt.Value, pred) } catch (NullReferenceException) { false } + let catchBlock = Expression.Catch (typeof, Expression.Constant (false)) + let tryExpr = Expression.TryCatch (anyCall, catchBlock) + tryExpr :> Expression + else + // Direct collection (not wrapped in option): pass directly to Enumerable.Any + let anyMethod = getEnumerableAnyMethod elementType + Expression.Call (anyMethod, rawCollExpr, lambda) + else + // Not a collection, treat as scalar + let paramExpr = Expression.PropertyOrField (param, f.FieldName) + buildFilterExpr isEnumerableQuery (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value + + + type private CompareDiscriminatorExpressionVisitor<'T, 'D> + (compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>, param : SourceExpression, value : obj) = + inherit ExpressionVisitor () + override _.VisitParameter (node) = + if node = compareDiscriminator.Parameters.[0] then + param.Value + elif node = compareDiscriminator.Parameters.[1] then + Expression.Constant (value) :> Expression + else + node :> Expression + + let enumerableQueryType = typedefof> + + let apply (options : ObjectListFilterLinqOptions<'T, 'D>) (filter : ObjectListFilter) (query : IQueryable<'T>) = + let isEnumerableQuery = query.GetType().GetGenericTypeDefinition () = enumerableQueryType + // Helper for discriminator comparison + let buildTypeDiscriminatorCheck (param : SourceExpression) (t : Type) = + match options.CompareDiscriminator, options.GetDiscriminatorValue with + | ValueNone, ValueNone -> + Expression.Equal ( + // Default discriminator property + Expression.PropertyOrField (param, "__typename"), + // Default discriminator value + Expression.Constant (t.FullName) + ) + :> Expression + | ValueSome discExpr, ValueNone -> + // Replace parameters from the original expression with our new ones + let replacer = CompareDiscriminatorExpressionVisitor (discExpr, param, t.FullName) + replacer.Visit discExpr.Body + | ValueNone, ValueSome discValueFn -> + let discriminatorValue = discValueFn t + Expression.Equal ( + // Default discriminator property + Expression.PropertyOrField (param, "__typename"), + // Provided discriminator value gathered from type + Expression.Constant (discriminatorValue) + ) + :> Expression + | ValueSome discExpr, ValueSome discValueFn -> + let discriminatorValue = discValueFn t + // Replace parameters from the original expression with our new ones + let replacer = CompareDiscriminatorExpressionVisitor (discExpr, param, discriminatorValue) + replacer.Visit discExpr.Body + let queryExpr = + let param = Expression.Parameter (typeof<'T>, "x") + let body = buildFilterExpr isEnumerableQuery (SourceExpression param) buildTypeDiscriminatorCheck filter + whereExpr<'T> query param body + // Create and execute the final expression + query.Provider.CreateQuery<'T> (queryExpr) + +[] +module ObjectListFilterExtensions = + + open ObjectListFilter + + type ObjectListFilter with + + /// + /// Applies the filter to a queryable with automatic type coercion of JSON primitives to CLR types. Supports , + /// , , , and F# discriminated unions. Pass + /// via ObjectListFilterLinqOptions constructor for custom serialization. + /// + /// + /// + /// // Basic usage - automatic coercion of string to Guid + /// let filter = "id" === "550e8400-e29b-41d4-a716-446655440000" + /// let users = filter.ApplyTo query + /// + /// // With custom JsonSerializerOptions + /// let opts = JsonSerializerOptions(PropertyNameCaseInsensitive = true) + /// let options = ObjectListFilterLinqOptions(opts) + /// let events = filter.ApplyTo(query, options) + /// + /// + member inline filter.ApplyTo<'T, 'D> (query : IQueryable<'T>, [] options : ObjectListFilterLinqOptions<'T, 'D> | null) = + let options = + options + |> ValueOption.ofObj + |> ValueOption.defaultValue ObjectListFilterLinqOptions<'T, 'D>.None + let filter = TypeCoercion.coerceFilter options.JsonOptions typeof<'T> filter + apply options filter query + + type IQueryable<'T> with + + /// + /// Applies the filter with automatic type coercion of JSON primitives to CLR types. Supports , , + /// , , and F# discriminated unions. Pass via + /// ObjectListFilterLinqOptions constructor for custom serialization. + /// + /// + /// + /// // Basic usage - automatic coercion of string to Guid + /// let filter = "id" === "550e8400-e29b-41d4-a716-446655440000" + /// let users = query.Apply filter + /// + /// // With custom JsonSerializerOptions + /// let opts = JsonSerializerOptions(PropertyNameCaseInsensitive = true) + /// let options = ObjectListFilterLinqOptions(opts) + /// let events = query.Apply(filter, options) + /// + /// + member inline query.Apply (filter : ObjectListFilter, [] options : ObjectListFilterLinqOptions<'T, 'D> | null) = + filter.ApplyTo (query, options) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs index 813c3b7bb..2fd6951dd 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs @@ -20,7 +20,6 @@ type private ComparisonOperator = | LessThanOrEqual of string | In of string - let rec private coerceObjectListFilterInput (variables : Variables) inputValue : Result = let parseFieldCondition (s : string) = @@ -184,7 +183,7 @@ let ObjectListFilterType : InputCustomDefinition = { (String.concat " " [ - "The ObjectListFilter value represents field filters for object lists." + "The `ObjectListFilter` value represents field filters for object lists." "Lowercase string suffixes such as `_starts_with`/`_sw`, `_ends_with`/`_ew`, `_contains` (no shorthand), and `_equals`/`_eq` are case-insensitive when applied to string fields." "Capitalized string suffixes such as `_Starts_With`/`_SW`, `_Ends_With`/`_EW`, `_Contains` (no shorthand), and `_Equals`/`_EQ` are case-sensitive when applied to string fields." "Comparison suffixes such as `_gt`, `_gte`, `_lt`, `_lte`, and `_in` are also supported." diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/TypeCoercion.fs b/src/FSharp.Data.GraphQL.Server.Middleware/TypeCoercion.fs new file mode 100644 index 000000000..78cc0f251 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.Middleware/TypeCoercion.fs @@ -0,0 +1,259 @@ +namespace FSharp.Data.GraphQL.Server.Middleware + +open System +open System.Buffers +open System.Collections.Generic +open System.Reflection +open System.Text.Json +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Extensions + +[] +module TypeCoercion = + + /// Case-insensitive instance property lookup. The middleware lowercases field names during + /// parsing, so we must also ignore casing here. + let propertyBindFlags = + BindingFlags.Public + ||| BindingFlags.Instance + ||| BindingFlags.IgnoreCase + + // Cached type references + let private stringType = typeof + + /// + /// If is voption, option, or Skippable, returns the inner type; otherwise . + /// + let tryUnwrapOption (t : Type) : Type voption = + if t.IsGenericType then + let fullName = t.GetGenericTypeDefinition().FullName + if + fullName.StartsWith ReflectionHelper.ValueOptionTypeName + || fullName.StartsWith ReflectionHelper.OptionTypeName + || fullName.StartsWith ReflectionHelper.SkippableTypeName + then + ValueSome (t.GetGenericArguments().[0]) + else + ValueNone + else + ValueNone + + let unwrapOption (t : Type) : Type = + tryUnwrapOption t |> ValueOption.defaultValue t + + /// + /// If is a generic collection, returns the element type. Handles both concrete collections (where IEnumerable is an + /// implemented interface) and properties typed directly as IEnumerable<T>. + /// + let tryUnwrapEnumerableElement (t : Type) : Type voption = + let isEnumerableInterface (i : Type) = + i.IsGenericType + && Type.(=) (i.GetGenericTypeDefinition (), typedefof>) + if Type.(=) (t, stringType) then + ValueNone + elif t.IsArray then + t.GetElementType () |> ValueOption.ofObj + elif isEnumerableInterface t then + ValueSome (t.GetGenericArguments()[0]) + else + t.GetInterfaces () + |> Array.vtryFind isEnumerableInterface + |> ValueOption.map (fun i -> i.GetGenericArguments()[0]) + + /// + /// Suffixes the middleware's parser preserves on FieldFilter.FieldName for scalar operators (e.g. meetingId_eq, + /// validFrom_gte). They must be stripped before resolving the actual CLR property. + /// These correspond to the lowercase variants produced after Phase 2 parsing in SchemaDefinitions.parseFieldCondition. Longer suffixes are + /// listed first to prevent shorter ones (e.g. _gt) from incorrectly matching longer ones (e.g. _gte). + /// + let operatorSuffixes = + [| + // String operators (case-insensitive variants) + FilterSuffixConstants.CI.StartsWithSuffix + FilterSuffixConstants.CI.EndsWithSuffix + FilterSuffixConstants.CI.SWSuffix + FilterSuffixConstants.CI.EWSuffix + FilterSuffixConstants.CI.ContainsSuffix + FilterSuffixConstants.CI.EqualsSuffix + FilterSuffixConstants.CI.EQSuffix + // String operators (case-sensitive variants) + FilterSuffixConstants.CS.StartsWithSuffix + FilterSuffixConstants.CS.EndsWithSuffix + FilterSuffixConstants.CS.SWSuffix + FilterSuffixConstants.CS.EWSuffix + FilterSuffixConstants.CS.ContainsSuffix + FilterSuffixConstants.CS.EqualsSuffix + FilterSuffixConstants.CS.EQSuffix + // Numeric/comparison operators (from root) + FilterSuffixConstants.GreaterThanOrEqualSuffix + FilterSuffixConstants.LessThanOrEqualSuffix + FilterSuffixConstants.GreaterThanSuffix + FilterSuffixConstants.LessThanSuffix + FilterSuffixConstants.GTESuffix + FilterSuffixConstants.LTESuffix + FilterSuffixConstants.GTSuffix + FilterSuffixConstants.LTSuffix + FilterSuffixConstants.InSuffix + |] + + let stripOperatorSuffix (fieldName : string) : string = + operatorSuffixes + |> Array.vtryFind (fun s -> fieldName.EndsWith (s, StringComparison.OrdinalIgnoreCase)) + |> ValueOption.map (fun s -> fieldName.Substring (0, fieldName.Length - s.Length)) + |> ValueOption.defaultValue fieldName + + /// + /// Writes a boxed GraphQL scalar primitive as a JSON token directly into . Strings become JSON strings; numbers and + /// booleans become raw JSON tokens. Returns true if the value was written; false if the type is unsupported. + /// + let private writeJsonValue (value : obj) (writer : Utf8JsonWriter) : bool = + match value with + | :? string as s -> + writer.WriteStringValue s + true + | :? bool as b -> + writer.WriteBooleanValue b + true + | :? int64 as n -> + writer.WriteNumberValue n + true + | :? int as n -> + writer.WriteNumberValue n + true + | :? double as n -> + writer.WriteNumberValue n + true + | :? float32 as n -> + writer.WriteNumberValue n + true + | :? decimal as n -> + writer.WriteNumberValue n + true + | _ -> + false + + // Suppress nullness warnings for the obj / objnull mixture. +#nowarn "3261" + /// + /// Tries to coerce a value into using STJ deserialization. Primitives are written directly as JSON bytes via + /// into an , then deserialized from ReadOnlySpan<byte>. + /// Already-correct values pass through unchanged. No intermediate string or is allocated. + /// + let tryCoerceValue (jsonOptions : JsonSerializerOptions voption) (targetType : Type) (value : objnull) : obj voption = + if isNull value then + ValueNone + elif targetType.IsInstanceOfType value then + ValueSome value + else + let buffer = ArrayBufferWriter 64 + use writer = new Utf8JsonWriter (buffer) + if not (writeJsonValue value writer) then + ValueNone + else + writer.Flush () + try + let opts = jsonOptions |> ValueOption.defaultValue JsonSerializerOptions.Default + JsonSerializer.Deserialize (buffer.WrittenSpan, targetType, opts) |> ValueSome + with _ -> + ValueNone + + /// + /// Coerces an entire tree recursively by resolving the entities's properties and converting filter values into the + /// property's CLR type. + /// + let rec coerceFilter (jsonOptions : JsonSerializerOptions voption) (entityType : Type) (filter : ObjectListFilter) : ObjectListFilter = + match filter with + | And (l, r) -> And (coerceFilter jsonOptions entityType l, coerceFilter jsonOptions entityType r) + | Or (l, r) -> Or (coerceFilter jsonOptions entityType l, coerceFilter jsonOptions entityType r) + | Not f -> Not (coerceFilter jsonOptions entityType f) + | OfTypes _ -> filter + | Equals (ff, cmp) -> + match entityType.GetProperty (stripOperatorSuffix ff.FieldName, propertyBindFlags) with + | null -> filter + | prop -> + let unwrapped = unwrapOption prop.PropertyType + match tryCoerceValue jsonOptions unwrapped (box ff.Value) with + | ValueNone -> filter + | ValueSome (:? IComparable as coerced) -> Equals ({ ff with Value = coerced }, cmp) + | ValueSome _ -> filter + | GreaterThan ff + | GreaterThanOrEqual ff + | LessThan ff + | LessThanOrEqual ff as originalFilter -> + match entityType.GetProperty (stripOperatorSuffix ff.FieldName, propertyBindFlags) with + | null -> filter + | prop -> + let unwrapped = unwrapOption prop.PropertyType + match tryCoerceValue jsonOptions unwrapped (box ff.Value) with + | ValueNone -> filter + | ValueSome (:? IComparable as coerced) -> + let coercedField = { ff with Value = coerced } + match originalFilter with + | GreaterThan _ -> GreaterThan coercedField + | GreaterThanOrEqual _ -> GreaterThanOrEqual coercedField + | LessThan _ -> LessThan coercedField + | LessThanOrEqual _ -> LessThanOrEqual coercedField + | _ -> filter + | ValueSome _ -> filter + | In ff -> + match entityType.GetProperty (stripOperatorSuffix ff.FieldName, propertyBindFlags) with + | null -> filter + | prop -> + let unwrapped = unwrapOption prop.PropertyType + + let struct (coercedValues, failedValues) = + ff.Value + |> List.fold + (fun struct (coerced, failed) value -> + match tryCoerceValue jsonOptions unwrapped value with + | ValueSome coercedValue -> (coercedValue :: coerced, failed) + | ValueNone -> struct (coerced, value :: failed)) + ([], []) + + match failedValues with + | [] -> In { ff with Value = List.rev coercedValues } + | _ -> + let failedValuesText = + failedValues + |> Seq.rev + |> Seq.map (sprintf "%A") + |> String.concat ", " + + invalidArg + (nameof filter) + ($"Unable to coerce one or more values for '{ff.FieldName}' to '{unwrapped.FullName}'. Uncoerced values: [{failedValuesText}]") + | StartsWith (ff, cmp) + | EndsWith (ff, cmp) as originalFilter -> + match entityType.GetProperty (stripOperatorSuffix ff.FieldName, propertyBindFlags) with + | null -> filter + | _ -> + match tryCoerceValue jsonOptions stringType (box ff.Value) with + | ValueNone -> filter + | ValueSome coerced -> + let coercedField = { ff with Value = coerced :?> string } + match originalFilter with + | StartsWith (_, cmp) -> StartsWith (coercedField, cmp) + | EndsWith (_, cmp) -> EndsWith (coercedField, cmp) + | _ -> filter + | Contains (ff, cmp) -> + match entityType.GetProperty (stripOperatorSuffix ff.FieldName, propertyBindFlags) with + | null -> filter + | prop -> + let unwrapped = unwrapOption prop.PropertyType + let coercionTarget = + match tryUnwrapEnumerableElement unwrapped with + | ValueSome elementType -> elementType + | ValueNone -> stringType + match tryCoerceValue jsonOptions coercionTarget (box ff.Value) with + | ValueNone -> filter + | ValueSome (:? IComparable as coerced) -> Contains ({ ff with Value = coerced }, cmp) + | ValueSome _ -> filter + | FilterField ff -> + match entityType.GetProperty (ff.FieldName, propertyBindFlags) with + | null -> filter + | prop -> + let unwrapped = unwrapOption prop.PropertyType + let nestedType = + tryUnwrapEnumerableElement unwrapped + |> ValueOption.defaultValue unwrapped + FilterField { FieldName = ff.FieldName; Value = coerceFilter jsonOptions nestedType ff.Value } diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs index 955684e1e..215e321fb 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs @@ -3,9 +3,7 @@ namespace FSharp.Data.GraphQL.Server.Middleware open System open System.Collections.Immutable open System.Linq -open FsToolkit.ErrorHandling -open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types /// Contains extensions for the type system. diff --git a/src/FSharp.Data.GraphQL.Shared/Helpers/Extensions.fs b/src/FSharp.Data.GraphQL.Shared/Helpers/Extensions.fs index 603ba60fc..b1a260995 100644 --- a/src/FSharp.Data.GraphQL.Shared/Helpers/Extensions.fs +++ b/src/FSharp.Data.GraphQL.Shared/Helpers/Extensions.fs @@ -85,6 +85,35 @@ module Array = i <- i + 1 Array.sub temp 0 i + /// + /// Attempts to find the first element in an array that satisfies the given predicate. + /// + /// Function to test each element. + /// The input array. + /// ValueSome of the first matching element, or ValueNone if no match is found. + let vtryFind predicate (source : 'T array) = + let mutable result = ValueNone + let mutable i = 0 + while i < source.Length && result.IsNone do + if predicate source[i] then + result <- ValueSome source[i] + i <- i + 1 + result + + /// + /// Applies a function to each element of an array and returns the first result where the function returns ValueSome. + /// + /// Function to apply to each element. + /// The input array. + /// ValueSome of the first successful mapping result, or ValueNone if no match is found. + let vtryPick mapping (source : 'T array) = + let mutable result = ValueNone + let mutable i = 0 + while i < source.Length && result.IsNone do + result <- mapping source[i] + i <- i + 1 + result + module List = /// @@ -99,6 +128,35 @@ module List = |> List.filter (fun x -> not <| List.exists(fun y -> f(x) = f(y)) listy) uniqx @ listy + /// + /// Attempts to find the first element in a list that satisfies the given predicate. + /// + /// Function to test each element. + /// The input list. + /// ValueSome of the first matching element, or ValueNone if no match is found. + let rec vtryFind predicate (source : 'T list) = + match source with + | [] -> ValueNone + | head :: tail -> + if predicate head then + ValueSome head + else + vtryFind predicate tail + + /// + /// Applies a function to each element of a list and returns the first result where the function returns ValueSome. + /// + /// Function to apply to each element. + /// The input list. + /// ValueSome of the first successful mapping result, or ValueNone if no match is found. + let rec vtryPick mapping (source : 'T list) = + match source with + | [] -> ValueNone + | head :: tail -> + match mapping head with + | ValueSome result -> ValueSome result + | ValueNone -> vtryPick mapping tail + module Set = /// 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 1bc224559..7242322a2 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -75,8 +75,14 @@ - - + + + + + + + + diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterComparerMappingTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterComparerMappingTests.fs new file mode 100644 index 000000000..c1ab68264 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterComparerMappingTests.fs @@ -0,0 +1,169 @@ +[] +[] +module FSharp.Data.GraphQL.Tests.ObjectListFilter.ComparerMapping.Tests + +open System +open System.Collections +open System.Globalization +open Xunit +open FSharp.Data.GraphQL.Server.Middleware + +// ───────────────────────────────────────────────────────────────────────────── +// Singleton reference-equality branch +// ───────────────────────────────────────────────────────────────────────────── + +[] +let ``comparerToStringComparison maps well-known StringComparer instances`` () = + let currentCultureIsInvariant = + CultureInfo.CurrentCulture.CompareInfo.Equals CultureInfo.InvariantCulture.CompareInfo + + let testCases = + [ + yield (StringComparer.OrdinalIgnoreCase :> IComparer, StringComparison.OrdinalIgnoreCase) + yield (StringComparer.InvariantCultureIgnoreCase :> IComparer, StringComparison.InvariantCultureIgnoreCase) + yield (StringComparer.Ordinal :> IComparer, StringComparison.Ordinal) + yield (StringComparer.InvariantCulture :> IComparer, StringComparison.InvariantCulture) + // On environments where CurrentCulture == InvariantCulture (e.g. Ubuntu CI with no locale), + // StringComparer.Current* singletons ARE the same objects as StringComparer.Invariant*, + // so they can only map to Invariant* values. Skip those cases in such environments. + if not currentCultureIsInvariant then + yield (StringComparer.CurrentCultureIgnoreCase :> IComparer, StringComparison.CurrentCultureIgnoreCase) + yield (StringComparer.CurrentCulture :> IComparer, StringComparison.CurrentCulture) + ] + + for comparer, expected in testCases do + let actual = ObjectListFilter.comparerToStringComparison comparer |> wantValueSome + actual |> equals expected + +// ───────────────────────────────────────────────────────────────────────────── +// Each singleton must map to a distinct StringComparison value +// ───────────────────────────────────────────────────────────────────────────── + +[] +let ``comparerToStringComparison singleton mappings are all distinct`` () = + let currentCultureIsInvariant = + CultureInfo.CurrentCulture.CompareInfo.Equals CultureInfo.InvariantCulture.CompareInfo + + // On environments where CurrentCulture == InvariantCulture, Current* singletons are + // the same objects as Invariant* ones, so distinctness can only be checked for the + // remaining four singletons. + let singletons : IComparer list = + [ + yield StringComparer.OrdinalIgnoreCase + yield StringComparer.InvariantCultureIgnoreCase + yield StringComparer.Ordinal + yield StringComparer.InvariantCulture + if not currentCultureIsInvariant then + yield StringComparer.CurrentCultureIgnoreCase + yield StringComparer.CurrentCulture + ] + + let results = + singletons + |> List.map (fun c -> ObjectListFilter.comparerToStringComparison c |> wantValueSome) + + let distinct = results |> List.distinct + List.length distinct |> equals (List.length results) + +// ───────────────────────────────────────────────────────────────────────────── +// IsWellKnownCultureAwareComparer fallback path +// StringComparer.Create produces a non-singleton comparer; the singleton +// ReferenceEquals fast path is skipped and IsWellKnownCultureAwareComparer +// is used instead. +// ───────────────────────────────────────────────────────────────────────────── + +[] +let ``comparerToStringComparison maps non-singleton InvariantCulture comparer`` () = + let comparer = StringComparer.Create (CultureInfo.InvariantCulture, false) :> IComparer + // must NOT be the same object as the singleton + Assert.False (obj.ReferenceEquals (comparer, StringComparer.InvariantCulture :> obj)) + let result = ObjectListFilter.comparerToStringComparison comparer |> wantValueSome + result |> equals StringComparison.InvariantCulture + +[] +let ``comparerToStringComparison maps non-singleton InvariantCultureIgnoreCase comparer`` () = + let comparer = StringComparer.Create (CultureInfo.InvariantCulture, true) :> IComparer + Assert.False (obj.ReferenceEquals (comparer, StringComparer.InvariantCultureIgnoreCase :> obj)) + let result = ObjectListFilter.comparerToStringComparison comparer |> wantValueSome + result |> equals StringComparison.InvariantCultureIgnoreCase + +[] +let ``comparerToStringComparison maps non-singleton CurrentCulture comparer`` () = + // When CurrentCulture == InvariantCulture (e.g. Ubuntu CI), a non-singleton comparer + // created from CurrentCulture is indistinguishable from InvariantCulture and will + // legitimately map to InvariantCulture. + let comparer = StringComparer.Create (CultureInfo.CurrentCulture, false) :> IComparer + Assert.False (obj.ReferenceEquals (comparer, StringComparer.CurrentCulture :> obj)) + let result = ObjectListFilter.comparerToStringComparison comparer |> wantValueSome + let currentCultureIsInvariant = + CultureInfo.CurrentCulture.CompareInfo.Equals CultureInfo.InvariantCulture.CompareInfo + let expected = + if currentCultureIsInvariant then StringComparison.InvariantCulture + else StringComparison.CurrentCulture + result |> equals expected + +[] +let ``comparerToStringComparison maps non-singleton CurrentCultureIgnoreCase comparer`` () = + let comparer = StringComparer.Create (CultureInfo.CurrentCulture, true) :> IComparer + Assert.False (obj.ReferenceEquals (comparer, StringComparer.CurrentCultureIgnoreCase :> obj)) + let result = ObjectListFilter.comparerToStringComparison comparer |> wantValueSome + let currentCultureIsInvariant = + CultureInfo.CurrentCulture.CompareInfo.Equals CultureInfo.InvariantCulture.CompareInfo + let expected = + if currentCultureIsInvariant then StringComparison.InvariantCultureIgnoreCase + else StringComparison.CurrentCultureIgnoreCase + result |> equals expected + +// ───────────────────────────────────────────────────────────────────────────── +// Unknown / unsupported cases → ValueNone +// ───────────────────────────────────────────────────────────────────────────── + +[] +let ``comparerToStringComparison returns ValueNone for null`` () = + ObjectListFilter.comparerToStringComparison null |> wantValueNone + +[] +let ``comparerToStringComparison returns ValueNone for non-StringComparer IComparer`` () = + let customComparer = + { new IComparer with + member _.Compare (_, _) = 0 + } + ObjectListFilter.comparerToStringComparison customComparer |> wantValueNone + +[] +let ``comparerToStringComparison returns ValueNone for non-standard culture comparer`` () = + // A comparer for a specific non-current, non-invariant culture — the + // IsWellKnownCultureAwareComparer fallback cannot map it to any of the six + // StringComparison values, so it must return ValueNone. + let trCulture = CultureInfo.GetCultureInfo "tr-TR" + // Only run this test when the test host is not Turkish (otherwise CurrentCulture == tr-TR + // and the result would legitimately be CurrentCulture). + if not (CultureInfo.CurrentCulture.Name.StartsWith "tr") then + let comparer = StringComparer.Create (trCulture, false) :> IComparer + ObjectListFilter.comparerToStringComparison comparer |> wantValueNone + +// ───────────────────────────────────────────────────────────────────────────── +// Determinism: calling comparerToStringComparison twice on the same instance +// must return the same result +// ───────────────────────────────────────────────────────────────────────────── + +[] +let ``comparerToStringComparison is deterministic for singletons`` () = + let currentCultureIsInvariant = + CultureInfo.CurrentCulture.CompareInfo.Equals CultureInfo.InvariantCulture.CompareInfo + + let singletons : IComparer list = + [ + yield StringComparer.OrdinalIgnoreCase + yield StringComparer.InvariantCultureIgnoreCase + yield StringComparer.Ordinal + yield StringComparer.InvariantCulture + if not currentCultureIsInvariant then + yield StringComparer.CurrentCultureIgnoreCase + yield StringComparer.CurrentCulture + ] + + for comparer in singletons do + let first = ObjectListFilter.comparerToStringComparison comparer + let second = ObjectListFilter.comparerToStringComparison comparer + first |> equals second diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqGenerateTests.fs similarity index 96% rename from tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs rename to tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqGenerateTests.fs index c8a392009..27d5332d4 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqGenerateTests.fs @@ -1,4 +1,6 @@ -module FSharp.Data.GraphQL.Tests.ObjectListFilterLinqGenerateTests +[] +[] +module FSharp.Data.GraphQL.Tests.ObjectListFilter.Linq.GenerateTests open Xunit open System @@ -99,6 +101,7 @@ let cosmosClient = new CosmosClient ("https://localhost:8081/", "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", options) let container = cosmosClient.GetContainer ("database", "container") let filterOptions = ObjectListFilterLinqOptions.None +let filterOptionsWithConverters = ObjectListFilterLinqOptions (jsonOptions) [] let ``ObjectListFilter works with Equals operator for ValidStringStruct`` () = @@ -245,13 +248,20 @@ let ``ObjectListFilter works with Contains operator for ValidStringStruct list`` equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ARRAY_CONTAINS(root["validStringStructList"], "athan")""" [] -let ``ObjectListFilter works with In operator for ValidStringStruct list`` () = +let ``ObjectListFilter works with In operator for ValidStringStruct list when converters are provided`` () = let queryable = container.GetItemLinqQueryable () let filter = In { FieldName = "validStringStruct"; Value = [ "athan"; "gaja" ] } - let filterQuery = queryable.Apply (filter, filterOptions) + let filterQuery = queryable.Apply (filter, filterOptionsWithConverters) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ARRAY_CONTAINS([ "athan", "gaja" ], root["validStringStruct"])""" +[] +let ``ObjectListFilter works with In operator for ValidStringStruct list when converters are not provided`` () = + let queryable = container.GetItemLinqQueryable () + let filter = In { FieldName = "validStringStruct"; Value = [ "athan"; "gaja" ] } + let ex = Assert.Throws(fun () -> queryable.Apply (filter, filterOptions) |> ignore) + Assert.Contains ("Uncoerced values", ex.Message) + [] let ``ObjectListFilter works with In operator for empty ValidStringStruct list`` () = let queryable = container.GetItemLinqQueryable () diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqTests.fs similarity index 99% rename from tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs rename to tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqTests.fs index bdd14b703..6990aefec 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqTests.fs @@ -1,4 +1,6 @@ -module FSharp.Data.GraphQL.Tests.ObjectListFilterLinqTests +[] +[] +module FSharp.Data.GraphQL.Tests.ObjectListFilter.Linq.Tests open Xunit open System diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterFieldEnumerableTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterFieldEnumerableTests.fs new file mode 100644 index 000000000..31bc7e0af --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterFieldEnumerableTests.fs @@ -0,0 +1,414 @@ +[] +[] +module FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.FieldEnumerableTests + +open System +open System.Linq +open System.Text.Json +open Xunit +open FSharp.Data.GraphQL.Shared +open FSharp.Data.GraphQL.Server.Middleware + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Test types for FilterField over enumerables +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +/// Entity with array collection. +type EntityWithArray = { + Id : int + Name : string + Tags : string array +} + +/// Entity with IEnumerable collection (via seq). +type EntityWithIEnumerable = { + Id : int + Name : string + Categories : string list +} + +/// Entity with option-wrapped array. +type EntityWithOptionalArray = { + Id : int + Name : string + OptionalTags : string array option +} + +/// Entity with nested object properties. +type NestedScore = { + Subject : string + Value : int +} + +/// Entity with nested collections. +type EntityWithNestedCollection = { + Id : int + Name : string + Scores : NestedScore array +} + +/// Entity using public fields instead of properties (to test field support in cache). +type EntityWithFields = + val Id : int + val mutable Name : string + val mutable Tags : string array + + new (id, name, tags) = { + Id = id + Name = name + Tags = tags + } + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Test data +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +let filterOptions = ObjectListFilterLinqOptions (Json.getSerializerOptions Seq.empty) +let filterOptionsIEnum = ObjectListFilterLinqOptions (Json.getSerializerOptions Seq.empty) +let filterOptionsOptArray = ObjectListFilterLinqOptions (Json.getSerializerOptions Seq.empty) +let filterOptionsNested = ObjectListFilterLinqOptions (Json.getSerializerOptions Seq.empty) +let filterOptionsFields = ObjectListFilterLinqOptions (Json.getSerializerOptions Seq.empty) + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Test data: arrays +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +let arrayTestData = [| + { Id = 1; Name = "Alice"; Tags = [| "admin"; "user" |] } + { Id = 2; Name = "Bob"; Tags = [| "user" |] } + { Id = 3; Name = "Charlie"; Tags = [||] } + { Id = 4; Name = "Diana"; Tags = [| "moderator"; "user" |] } +|] + +let applyFilterArray (filter : ObjectListFilter) = + filter.ApplyTo (arrayTestData.AsQueryable (), filterOptions) + |> Seq.toList + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Test data: IEnumerable (list) +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +let ienumeTestData = [| + { Id = 1; Name = "Alice"; Categories = [ "Books"; "Movies" ] } + { Id = 2; Name = "Bob"; Categories = [ "Sports" ] } + { Id = 3; Name = "Charlie"; Categories = [] } + { Id = 4; Name = "Diana"; Categories = [ "Music"; "Sports" ] } +|] + +let applyFilterIEnum (filter : ObjectListFilter) = + filter.ApplyTo (ienumeTestData.AsQueryable (), filterOptionsIEnum) + |> Seq.toList + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Test data: option-wrapped arrays +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +let optArrayTestData = [| + { Id = 1; Name = "Alice"; OptionalTags = Some [| "admin"; "user" |] } + { Id = 2; Name = "Bob"; OptionalTags = Some [| "user" |] } + { Id = 3; Name = "Charlie"; OptionalTags = None } + { Id = 4; Name = "Diana"; OptionalTags = Some [||] } +|] + +let applyFilterOptArray (filter : ObjectListFilter) = + filter.ApplyTo (optArrayTestData.AsQueryable (), filterOptionsOptArray) + |> Seq.toList + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Test data: nested collections +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +let nestedTestData = [| + { Id = 1; Name = "Alice"; Scores = [| { Subject = "Math"; Value = 95 }; { Subject = "Science"; Value = 88 } |] } + { Id = 2; Name = "Bob"; Scores = [| { Subject = "Math"; Value = 75 }; { Subject = "Science"; Value = 82 } |] } + { Id = 3; Name = "Charlie"; Scores = [| { Subject = "English"; Value = 90 } |] } + { Id = 4; Name = "Diana"; Scores = [||] } +|] + +let applyFilterNested (filter : ObjectListFilter) = + filter.ApplyTo (nestedTestData.AsQueryable (), filterOptionsNested) + |> Seq.toList + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Test data: public fields (not properties) +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +let fieldsTestData = [| + EntityWithFields(1, "Alice", [| "admin"; "user" |]) + EntityWithFields(2, "Bob", [| "user" |]) + EntityWithFields(3, "Charlie", [||]) + EntityWithFields(4, "Diana", [| "moderator"; "user" |]) +|] + +let applyFilterFields (filter : ObjectListFilter) = + filter.ApplyTo (fieldsTestData.AsQueryable (), filterOptionsFields) + |> Seq.toList + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Helpers for scalar collection filters +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +/// Creates a filter for scalar collections (array, list, etc.) with a nested operator. +/// +/// IMPORTANT: When filtering scalar collection elements (e.g., string array or IEnumerable), +/// the inner filter's FieldName must use "_" as a placeholder, since scalar elements don't have properties. +/// +/// Example: +/// scalarCollectionFilter "Tags" (In { FieldName = "_"; Value = [box "admin"] }) +/// // Filters entities where the Tags collection contains "admin" +/// +/// The middleware's Enumerable.Any operator treats "_" as a no-op and evaluates the scalar element directly. +let scalarCollectionFilter (fieldName : string) (innerFilter : ObjectListFilter) : ObjectListFilter = + FilterField { + FieldName = fieldName + Value = innerFilter + } + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Tests: FilterField over arrays +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +[] +let ``FilterField over array with In finds matching elements`` () = + let filter = + scalarCollectionFilter + "Tags" + (In { FieldName = "_"; Value = [ box "admin" ] }) + let result = applyFilterArray filter + + Assert.Equal (1, result.Length) + Assert.Equal ("Alice", result[0].Name) + +[] +let ``FilterField over array with In finds multiple matching values`` () = + let filter = + scalarCollectionFilter + "Tags" + (In { FieldName = "_"; Value = [ box "user"; box "moderator" ] }) + let result = applyFilterArray filter + + Assert.Equal (3, result.Length) + let names = result |> List.map (fun e -> e.Name) |> List.sort |> List.toSeq + Assert.Equal> (seq { "Alice"; "Bob"; "Diana" }, names) + +[] +let ``FilterField over array with In excludes empty arrays`` () = + let filter = + scalarCollectionFilter + "Tags" + (In { FieldName = "_"; Value = [ box "user" ] }) + let result = applyFilterArray filter + + let names = result |> List.map (fun e -> e.Name) + Assert.False (names.Contains "Charlie") + +[] +let ``FilterField over array with Equals finds exact match`` () = + let filter = + scalarCollectionFilter + "Tags" + (Equals ({ FieldName = "_"; Value = "admin" }, null)) + let result = applyFilterArray filter + + Assert.Equal (1, result.Length) + Assert.Equal ("Alice", result[0].Name) + +[] +let ``FilterField over array with StartsWith finds prefix matches`` () = + // Note: StartsWith only works on string members, not on the element itself (_) + // This test documents that FilterField with StartsWith requires a named property + // and won't work with scalar element filters + let filter = + scalarCollectionFilter + "Tags" + (In { FieldName = "_"; Value = [ box "user"; box "admin" ] }) + let result = applyFilterArray filter + + Assert.Equal (3, result.Length) + let names = result |> List.map (fun e -> e.Name) |> List.sort |> List.toSeq + Assert.Equal> (seq { "Alice"; "Bob"; "Diana" }, names) + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Tests: FilterField over IEnumerable (list) +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +[] +let ``FilterField over IEnumerable list with In finds matching elements`` () = + let filter = + scalarCollectionFilter + "Categories" + (In { FieldName = "_"; Value = [ box "Sports" ] }) + let result = applyFilterIEnum filter + + Assert.Equal (2, result.Length) + let names = result |> List.map (fun e -> e.Name) |> List.sort |> List.toSeq + Assert.Equal> (seq { "Bob"; "Diana" }, names) + +[] +let ``FilterField over IEnumerable with Equals finds exact match`` () = + let filter = + scalarCollectionFilter + "Categories" + (Equals ({ FieldName = "_"; Value = "Books" }, null)) + let result = applyFilterIEnum filter + + Assert.Equal (1, result.Length) + Assert.Equal ("Alice", result[0].Name) + +[] +let ``FilterField over IEnumerable excludes empty collections`` () = + let filter = + scalarCollectionFilter + "Categories" + (In { FieldName = "_"; Value = [ box "Books" ] }) + let result = applyFilterIEnum filter + + let names = result |> List.map (fun e -> e.Name) + Assert.False (names.Contains "Charlie") + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Tests: FilterField over option-wrapped arrays +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +[] +let ``FilterField over optional array with In finds matching elements`` () = + let filter = + scalarCollectionFilter + "OptionalTags" + (In { FieldName = "_"; Value = [ box "admin" ] }) + let result = applyFilterOptArray filter + + Assert.Equal (1, result.Length) + Assert.Equal ("Alice", result[0].Name) + +[] +let ``FilterField over optional array skips None values`` () = + let filter = + scalarCollectionFilter + "OptionalTags" + (In { FieldName = "_"; Value = [ box "user" ] }) + let result = applyFilterOptArray filter + + let names = result |> List.map (fun e -> e.Name) + Assert.False (names.Contains "Charlie") // Charlie has None + +[] +let ``FilterField over optional array skips empty inner arrays`` () = + let filter = + scalarCollectionFilter + "OptionalTags" + (In { FieldName = "_"; Value = [ box "admin" ] }) + let result = applyFilterOptArray filter + + let names = result |> List.map (fun e -> e.Name) + Assert.False (names.Contains "Diana") // Diana has Some [||] + +[] +let ``FilterField over optional array with Equals finds exact match in Some`` () = + let filter = + scalarCollectionFilter + "OptionalTags" + (Equals ({ FieldName = "_"; Value = "user" }, null)) + let result = applyFilterOptArray filter + + Assert.Equal (2, result.Length) + let names = result |> List.map (fun e -> e.Name) |> List.sort |> List.toSeq + Assert.Equal> (seq { "Alice"; "Bob" }, names) + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Tests: FilterField with nested collection properties +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +[] +let ``FilterField over nested collection with nested Equals finds by nested property`` () = + let filter = + FilterField { + FieldName = "Scores" + Value = FilterField { + FieldName = "Subject" + Value = Equals ({ FieldName = "_"; Value = "Math" }, null) + } + } + let result = applyFilterNested filter + + Assert.Equal (2, result.Length) + let names = result |> List.map (fun e -> e.Name) |> List.sort |> List.toSeq + Assert.Equal> (seq { "Alice"; "Bob" }, names) + +[] +let ``FilterField over nested collection with GreaterThan`` () = + let filter = + FilterField { + FieldName = "Scores" + Value = GreaterThan { FieldName = "Value"; Value = 88 } + } + let result = applyFilterNested filter + + Assert.Equal (2, result.Length) + let names = result |> List.map (fun e -> e.Name) |> List.sort |> List.toSeq + Assert.Equal> (seq { "Alice"; "Charlie" }, names) + +[] +let ``FilterField over nested collection excludes empty nested arrays`` () = + let filter = + FilterField { + FieldName = "Scores" + Value = Equals ({ FieldName = "Subject"; Value = "Math" }, null) + } + let result = applyFilterNested filter + + let names = result |> List.map (fun e -> e.Name) + Assert.False (names.Contains "Diana") // Diana has empty Scores + +[] +let ``FilterField over nested collection with multiple criteria`` () = + let filter = + FilterField { + FieldName = "Scores" + Value = And ( + GreaterThan { FieldName = "Value"; Value = 70 }, + Equals ({ FieldName = "Subject"; Value = "Math" }, null) + ) + } + let result = applyFilterNested filter + + Assert.Equal (2, result.Length) + let names = result |> List.map (fun e -> e.Name) |> List.sort |> List.toSeq + Assert.Equal> (seq { "Alice"; "Bob" }, names) + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Tests: FilterField over public fields (verifying field resolution in cache) +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +[] +let ``FilterField over public field array with In finds matching elements`` () = + let filter = + scalarCollectionFilter + "Tags" + (In { FieldName = "_"; Value = [ box "admin" ] }) + let result = applyFilterFields filter + + Assert.Equal (1, result.Length) + Assert.Equal ("Alice", result[0].Name) + +[] +let ``FilterField over public field array with Equals finds exact match`` () = + let filter = + scalarCollectionFilter + "Tags" + (Equals ({ FieldName = "_"; Value = "moderator" }, null)) + let result = applyFilterFields filter + + Assert.Equal (1, result.Length) + Assert.Equal ("Diana", result[0].Name) + +[] +let ``FilterField over public field array excludes empty fields`` () = + let filter = + scalarCollectionFilter + "Tags" + (In { FieldName = "_"; Value = [ box "user" ] }) + let result = applyFilterFields filter + + let names = result |> List.map (fun e -> e.Name) + Assert.False (names.Contains "Charlie") diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterInOperatorTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterInOperatorTests.fs new file mode 100644 index 000000000..400f04a66 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterInOperatorTests.fs @@ -0,0 +1,235 @@ +[] +[] +module FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.InOperatorTests + +open System +open System.Linq +open System.Linq.Expressions +open Xunit +open FSharp.Data.GraphQL.Shared +open FSharp.Data.GraphQL.Server.Middleware +open FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.Common + +// ────────────────────────────────────────────────────────────────────────────── +// In operator test cases +// Covers all coercion types: CLR enum, DU-as-enum, Guid, single-case DU, and primitives +// ────────────────────────────────────────────────────────────────────────────── + +/// Minimal entity model used to validate `In` translation for nullable fields. +type NullableEntity = { + /// Primary identifier used in test assertions. + Id: int + /// Nullable scalar field used to assert typed `Contains>` expression generation. + MaybeId: Nullable +} + +/// Query options for nullable-expression tests with JSON coercion enabled. +let private nullableOptions = + ObjectListFilterLinqOptions (Json.getSerializerOptions Seq.empty) + +/// In-memory source for nullable-field `In` tests. +let private nullableData = + [| + { Id = 1; MaybeId = Nullable 1 } + { Id = 2; MaybeId = Nullable() } + { Id = 3; MaybeId = Nullable 3 } + |] + +/// Applies an `ObjectListFilter` to the nullable-field test dataset. +let private applyNullableFilter (filter : ObjectListFilter) = + filter.ApplyTo (nullableData.AsQueryable (), nullableOptions) |> Seq.toList + +/// Traverses an expression tree and returns the first `Enumerable.Contains` call node. +let private tryFindEnumerableContainsCall (expr : Expression) : MethodCallExpression option = + let rec find (node : Expression) = + match node with + | :? MethodCallExpression as call + when call.Method.Name = "Contains" + && call.Method.DeclaringType = typeof + && call.Arguments.Count = 2 -> + Some call + | :? MethodCallExpression as call -> + let fromObject = + if isNull call.Object then None else find call.Object + match fromObject with + | Some found -> Some found + | None -> call.Arguments |> Seq.tryPick find + | :? UnaryExpression as unary -> find unary.Operand + | :? LambdaExpression as lambda -> find lambda.Body + | :? BinaryExpression as binary -> + match find binary.Left with + | Some found -> Some found + | None -> find binary.Right + | :? MemberExpression as memberExpr -> + if isNull memberExpr.Expression then None else find memberExpr.Expression + | _ -> None + find expr + +/// Builds a filtered query and extracts the generated `Enumerable.Contains` call from its expression tree. +let private findEnumerableContainsCall<'T, 'D> (options : ObjectListFilterLinqOptions<'T, 'D>) (filter : ObjectListFilter) = + let query = Enumerable.Empty<'T>().AsQueryable() + let result = query.Apply (filter, options) + match tryFindEnumerableContainsCall result.Expression with + | Some call -> call + | None -> + fail "Expected to find Enumerable.Contains call in generated expression tree" + Unchecked.defaultof + +/// Asserts that `In` is translated to a strongly typed `Enumerable.Contains` expression. +/// Validates method generic argument, typed values container, and non-boxed member argument. +let private assertTypedInExpression<'T, 'D> + (options : ObjectListFilterLinqOptions<'T, 'D>) + (filter : ObjectListFilter) + (expectedElementType : Type) + = + let containsCall = findEnumerableContainsCall options filter + containsCall.Method.GetGenericArguments().[0] |> equals expectedElementType + + let valuesArgType = containsCall.Arguments.[0].Type + if valuesArgType = typeof || valuesArgType = typeof then + fail $"Expected a strongly typed values argument, but got {valuesArgType.FullName}" + + let actualElementType = + if valuesArgType.IsArray then + valuesArgType.GetElementType () + elif valuesArgType.IsGenericType then + valuesArgType.GetGenericArguments().[0] + else + fail $"Expected array or generic collection values argument, got {valuesArgType.FullName}" + Unchecked.defaultof + + actualElementType |> equals expectedElementType + + match containsCall.Arguments.[1] with + | :? UnaryExpression as unary when unary.NodeType = ExpressionType.Convert && unary.Type = typeof -> + fail "Expected In member argument to remain strongly typed without boxing to object" + | _ -> () + +[] +let ``In operator coerces string primitives`` () = + let filter = In { FieldName = "name"; Value = [ box "Alice"; box "Bob" ] } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Bob" ] + +[] +let ``In operator coerces int primitives`` () = + let filter = In { FieldName = "id"; Value = [ box 1; box 3 ] } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Id) |> List.sort |> equals [ 1; 3 ] + +[] +let ``In operator coerces CLR enum`` () = + let filter = In { FieldName = "color"; Value = [ box "Red"; box "Blue" ] } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ] + +[] +let ``In operator coerces DU-as-enum`` () = + let filter = In { FieldName = "status"; Value = [ box "Active"; box "Pending" ] } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ] + +[] +let ``In operator coerces Guid`` () = + let filter = + In { + FieldName = "guidField" + Value = [ + box "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + box "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + ] + } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Bob" ] + +[] +let ``In operator coerces single-case DU wrapping string`` () = + let filter = In { FieldName = "wrappedName"; Value = [ box "Alice"; box "Charlie" ] } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ] + +[] +let ``In operator coerces single-case DU wrapping int`` () = + let filter = In { FieldName = "wrappedScore"; Value = [ box 10; box 30 ] } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ] + +[] +let ``In operator coerces single-case DU wrapping Guid`` () = + let filter = + In { + FieldName = "wrappedGuid" + Value = [ box "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; box "cccccccc-cccc-cccc-cccc-cccccccccccc" ] + } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ] + +[] +let ``In operator coerces Nullable int primitives`` () = + let filter = In { FieldName = "maybeId"; Value = [ box 1; box 3 ] } + let result = applyNullableFilter filter + result |> List.map (fun e -> e.Id) |> List.sort |> equals [ 1; 3 ] + +[] +let ``In operator expression uses typed contains for string primitive field`` () = + let filter = In { FieldName = "name"; Value = [ box "Alice"; box "Bob" ] } + assertTypedInExpression filterOptions filter typeof + +[] +let ``In operator expression uses typed contains for int primitive field`` () = + let filter = In { FieldName = "id"; Value = [ box 1; box 3 ] } + assertTypedInExpression filterOptions filter typeof + +[] +let ``In operator expression uses typed contains for CLR enum field`` () = + let filter = In { FieldName = "color"; Value = [ box "Red"; box "Blue" ] } + assertTypedInExpression filterOptions filter typeof + +[] +let ``In operator expression uses typed contains for fieldless DU field`` () = + let filter = In { FieldName = "status"; Value = [ box "Active"; box "Pending" ] } + assertTypedInExpression filterOptions filter typeof + +[] +let ``In operator expression uses typed contains for Guid field`` () = + let filter = In { FieldName = "guidField"; Value = [ box "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" ] } + assertTypedInExpression filterOptions filter typeof + +[] +let ``In operator expression uses typed contains for single-case DU string field`` () = + let filter = In { FieldName = "wrappedName"; Value = [ box "Alice"; box "Charlie" ] } + assertTypedInExpression filterOptions filter typeof + +[] +let ``In operator expression uses typed contains for single-case DU int field`` () = + let filter = In { FieldName = "wrappedScore"; Value = [ box 10; box 30 ] } + assertTypedInExpression filterOptions filter typeof + +[] +let ``In operator expression uses typed contains for single-case DU Guid field`` () = + let filter = In { FieldName = "wrappedGuid"; Value = [ box "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" ] } + assertTypedInExpression filterOptions filter typeof + +[] +let ``In operator expression uses typed contains for option field`` () = + let filter = In { FieldName = "optionName"; Value = [ box "Alice"; box "Charlie" ] } + assertTypedInExpression filterOptions filter typeof + +[] +let ``In operator expression uses typed contains for voption field`` () = + let filter = In { FieldName = "vOptionId"; Value = [ box 1; box 3 ] } + assertTypedInExpression filterOptions filter typeof + +[] +let ``In operator expression uses typed contains for Nullable field`` () = + let filter = In { FieldName = "maybeId"; Value = [ box 1; box 3 ] } + assertTypedInExpression nullableOptions filter typeof> + diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterOptionCollectionTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterOptionCollectionTests.fs new file mode 100644 index 000000000..d8b32768a --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterOptionCollectionTests.fs @@ -0,0 +1,106 @@ +[] +[] +module FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.OptionCollectionTests + +open System +open System.Linq +open System.Text.Json +open Xunit +open FSharp.Data.GraphQL.Shared +open FSharp.Data.GraphQL.Server.Middleware + +// ──────────────────────────────────────────────────────────────────────────────────── +// Test types for option collection filters +// ──────────────────────────────────────────────────────────────────────────────────── + +/// Tags wrapped in an option — tests FilterField with optional collection. +type OptionalTagsEntity = { + Id : int + Name : string + /// Optional list of tags — tests the edge case where the field itself is optional + OptionalTags : string list option +} + +// ──────────────────────────────────────────────────────────────────────────────────── +// Test data +// ──────────────────────────────────────────────────────────────────────────────────── + +let filterOptions = + ObjectListFilterLinqOptions (Json.getSerializerOptions Seq.empty) + +let testData = [| + { Id = 1; Name = "Alice"; OptionalTags = Some [ "admin"; "user" ] } + { Id = 2; Name = "Bob"; OptionalTags = Some [ "user" ] } + { Id = 3; Name = "Charlie"; OptionalTags = None } + { Id = 4; Name = "Diana"; OptionalTags = Some [] } +|] + +let applyFilter (filter : ObjectListFilter) = + filter.ApplyTo (testData.AsQueryable (), filterOptions) + |> Seq.toList + +// ──────────────────────────────────────────────────────────────────────────────────── +// Tests for FilterField with optional collections +// ──────────────────────────────────────────────────────────────────────────────────── + +[] +let ``FilterField on optional collection with In operator returns entities where tag matches`` () = + // Regression test: FilterField on optional collection should unwrap the option + // and correctly apply the nested filter to the contained collection + let filter = + FilterField { + FieldName = "OptionalTags" + Value = In { FieldName = "_"; Value = [ box "admin" ] } + } + let result = applyFilter filter + + Assert.Equal (1, result.Length) + Assert.Equal> ([| "Alice" |] :> seq<_>, result |> List.map (fun e -> e.Name) |> List.toSeq) + +[] +let ``FilterField on optional collection with multiple values`` () = + let filter = + FilterField { + FieldName = "OptionalTags" + Value = In { FieldName = "_"; Value = [ box "admin"; box "moderator" ] } + } + let result = applyFilter filter + + Assert.Equal (1, result.Length) + Assert.Equal> ([| "Alice" |] :> seq<_>, result |> List.map (fun e -> e.Name) |> List.toSeq) + +[] +let ``FilterField on optional collection skips None values`` () = + let filter = + FilterField { + FieldName = "OptionalTags" + Value = In { FieldName = "_"; Value = [ box "user" ] } + } + let result = applyFilter filter + + Assert.Equal (2, result.Length) + Assert.Equal> ([| "Alice"; "Bob" |] :> seq<_>, result |> Seq.map (fun e -> e.Name) |> Seq.sort) + +[] +let ``FilterField on optional collection with empty list`` () = + let filter = + FilterField { + FieldName = "OptionalTags" + Value = In { FieldName = "_"; Value = [ box "user" ] } + } + let result = applyFilter filter + + let names = result |> List.map (fun e -> e.Name) + Assert.False (names.Contains "Diana") + +[] +let ``FilterField on optional collection with Equals operator`` () = + let filter = + FilterField { + FieldName = "OptionalTags" + Value = Equals ({ FieldName = "_"; Value = "user" }, null) + } + let result = applyFilter filter + + Assert.Equal (2, result.Length) + Assert.Equal> ([| "Alice"; "Bob" |] :> seq<_>, result |> Seq.map (fun e -> e.Name) |> Seq.sort) diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterTests.fs new file mode 100644 index 000000000..e37fe18a2 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterTests.fs @@ -0,0 +1,231 @@ +[] +module FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.FilterTests + +open Xunit +open System +open FSharp.Data.GraphQL.Server.Middleware +open FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.Common + +// ────────────────────────────────────────────────────────────────────────────── +// Equals operator +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``coerceFilter coerces string to Guid for Equals`` () = + let filter = Equals ({ FieldName = "guidField"; Value = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Alice" + +[] +let ``coerceFilter coerces string to CLR enum for Equals`` () = + let filter = Equals ({ FieldName = "color"; Value = "Green" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Bob" + +[] +let ``coerceFilter coerces int to CLR enum for Equals`` () = + let filter = Equals ({ FieldName = "color"; Value = 2 }, null) // Blue = 2 + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Charlie" + +[] +let ``coerceFilter coerces string to DU-as-enum for Equals`` () = + let filter = Equals ({ FieldName = "status"; Value = "Active" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Alice" + +[] +let ``coerceFilter coerces string to single-case DU wrapping string for Equals`` () = + let filter = Equals ({ FieldName = "wrappedName"; Value = "Bob" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Bob" + +[] +let ``coerceFilter coerces int to single-case DU wrapping int for Equals`` () = + let filter = Equals ({ FieldName = "wrappedScore"; Value = 30 }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Charlie" + +[] +let ``coerceFilter coerces int64 to single-case DU wrapping int64 for Equals`` () = + let filter = Equals ({ FieldName = "wrappedLong"; Value = 200L }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Bob" + +[] +let ``coerceFilter coerces string to single-case DU wrapping Guid for Equals`` () = + let filter = Equals ({ FieldName = "wrappedGuid"; Value = "cccccccc-cccc-cccc-cccc-cccccccccccc" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Charlie" + +[] +let ``coerceFilter passes through bool for Equals`` () = + let filter = Equals ({ FieldName = "isActive"; Value = true }, null) + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.forall (fun e -> e.IsActive) |> equals true + +// ────────────────────────────────────────────────────────────────────────────── +// comparison operators (GreaterThan / LessThan) +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``coerceFilter coerces int to decimal for GreaterThanOrEqual`` () = + // Score: Alice=100, Bob=200, Charlie=300. >= 200 -> Bob and Charlie + let filter = GreaterThanOrEqual { FieldName = "score"; Value = 200 } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Bob"; "Charlie" ] + +[] +let ``coerceFilter coerces string to DateTime for GreaterThan`` () = + // CreatedAt: Alice=2024-01-01, Bob=2024-02-01, Charlie=2024-03-01. > 2024-01-15 + let filter = GreaterThan { FieldName = "createdAt"; Value = "2024-01-15T00:00:00" } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Bob"; "Charlie" ] + +[] +let ``coerceFilter coerces string to DateOnly for LessThan`` () = + // BirthDate: Alice=1990-05-15, Bob=1985-08-20, Charlie=2000-12-31. < 2000-01-01 + let filter = LessThan { FieldName = "birthDate"; Value = "2000-01-01" } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Bob" ] + +[] +let ``coerceFilter coerces string to TimeOnly for GreaterThan`` () = + // AlarmTime: Alice=08:00, Bob=09:00, Charlie=10:00. > 08:30 -> Bob and Charlie + let filter = GreaterThan { FieldName = "alarmTime"; Value = "08:30:00" } + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Bob"; "Charlie" ] + +// ────────────────────────────────────────────────────────────────────────────── +// option / voption field unwrapping +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``coerceFilter coerces string through option wrapper for Equals`` () = + let filter = Equals ({ FieldName = "optionName"; Value = "Alice" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Alice" + +[] +let ``coerceFilter coerces int through voption wrapper for Equals`` () = + let filter = Equals ({ FieldName = "vOptionId"; Value = 3 }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Charlie" + +// ────────────────────────────────────────────────────────────────────────────── +// string operators +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``coerceFilter handles StartsWith for plain string field`` () = + let filter = StartsWith ({ FieldName = "name"; Value = "Al" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Alice" + +[] +let ``coerceFilter handles EndsWith for plain string field`` () = + let filter = EndsWith ({ FieldName = "name"; Value = "ie" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Charlie" + +[] +let ``coerceFilter handles Contains for plain string field`` () = + let filter = Contains ({ FieldName = "name"; Value = "ob" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Bob" + +[] +let ``coerceFilter handles Contains for list field (element membership)`` () = + // Tags: Alice=["admin";"user"], Bob=["user"], Charlie=["moderator";"user"] + let filter = Contains ({ FieldName = "tags"; Value = "admin" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Alice" + +[] +let ``coerceFilter handles Contains for list field matching multiple entities`` () = + let filter = Contains ({ FieldName = "tags"; Value = "user" }, null) + let result = applyFilter filter + result |> List.length |> equals 3 + +// ────────────────────────────────────────────────────────────────────────────── +// AND / OR / NOT combinators +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``coerceFilter coerces values inside AND`` () = + let filter = + And ( + Equals ({ FieldName = "color"; Value = "Red" }, null), + Equals ({ FieldName = "status"; Value = "Active" }, null) + ) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Alice" + +[] +let ``coerceFilter coerces values inside OR`` () = + let filter = + Or ( + Equals ({ FieldName = "color"; Value = "Red" }, null), + Equals ({ FieldName = "color"; Value = "Blue" }, null) + ) + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ] + +[] +let ``coerceFilter coerces values inside NOT`` () = + let filter = Not (Equals ({ FieldName = "status"; Value = "Active" }, null)) + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Bob"; "Charlie" ] + +// ────────────────────────────────────────────────────────────────────────────── +// StringComparer on non-string field must not throw InvalidCastException +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``Equals with StringComparer on non-string field does not throw InvalidCastException`` () = + // WrappedGuid is not a string; passing StringComparer.OrdinalIgnoreCase used to + // crash with InvalidCastException because the code unconditionally cast f.Value + // to string when a StringComparer was present. + let filter = + Equals ( + { FieldName = "wrappedGuid"; Value = WrappedGuid (Guid.Parse "cccccccc-cccc-cccc-cccc-cccccccccccc") }, + StringComparer.OrdinalIgnoreCase + ) + let result = applyFilter filter + result |> List.length |> equals 1 + (List.head result).Name |> equals "Charlie" + +[] +let ``Not Equals with StringComparer on non-string field does not throw InvalidCastException`` () = + let filter = + Not ( + Equals ( + { FieldName = "wrappedGuid"; Value = WrappedGuid (Guid.Parse "cccccccc-cccc-cccc-cccc-cccccccccccc") }, + StringComparer.OrdinalIgnoreCase + ) + ) + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Bob" ] diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionTests.Common.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionTests.Common.fs new file mode 100644 index 000000000..f78aa54c8 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionTests.Common.fs @@ -0,0 +1,136 @@ +[] +module FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.Common + +open System +open System.Linq +open System.Text.Json +open FSharp.Data.GraphQL.Shared +open FSharp.Data.GraphQL.Server.Middleware + +// ────────────────────────────────────────────────────────────────────────────── +// Test types +// ────────────────────────────────────────────────────────────────────────────── + +/// CLR enum — coerced from string via JsonStringEnumConverter or from int via default STJ. +type Color = + | Red = 0 + | Green = 1 + | Blue = 2 + +/// Multi-case fieldless DU (DU-as-enum) — coerced from string via +/// FSharp.SystemTextJson UnwrapFieldlessTags. +type Status = + | Active + | Inactive + | Pending + +/// Single-case DU wrapping a string — coerced via FSharp.SystemTextJson UnwrapSingleCaseUnions. +type WrappedString = WrappedString of string + +/// Single-case DU wrapping an int. +type WrappedInt = WrappedInt of int + +/// Single-case DU wrapping an int64. +type WrappedInt64 = WrappedInt64 of int64 + +/// Single-case DU wrapping a Guid. +type WrappedGuid = WrappedGuid of Guid + +/// Entity used in coerceFilter integration tests. +type CoercionEntity = { + Id: int + Name: string + Status: Status + Color: Color + GuidField: Guid + WrappedName: WrappedString + WrappedScore: WrappedInt + WrappedLong: WrappedInt64 + WrappedGuid: WrappedGuid + CreatedAt: DateTime + BirthDate: DateOnly + AlarmTime: TimeOnly + Score: decimal + IsActive: bool + Tags: string list + OptionName: string option + VOptionId: int voption +} + +// ────────────────────────────────────────────────────────────────────────────── +// Shared test infrastructure +// ────────────────────────────────────────────────────────────────────────────── + +/// Full serializer options including FSharp.SystemTextJson (DU coercion) and +/// JsonStringEnumConverter (CLR enum coercion). +let jsonOptions = ValueSome (Json.getSerializerOptions Seq.empty) + +/// No options — uses STJ defaults; sufficient for primitives, Guid, date/time. +let noOptions : JsonSerializerOptions voption = ValueNone + +let filterOptions = + ObjectListFilterLinqOptions (Json.getSerializerOptions Seq.empty) + +let testData = + [| + { + Id = 1 + Name = "Alice" + Status = Active + Color = Color.Red + GuidField = Guid.Parse "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + WrappedName = WrappedString "Alice" + WrappedScore = WrappedInt 10 + WrappedLong = WrappedInt64 100L + WrappedGuid = WrappedGuid (Guid.Parse "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + CreatedAt = DateTime (2024, 1, 1) + BirthDate = DateOnly (1990, 5, 15) + AlarmTime = TimeOnly (8, 0) + Score = 100m + IsActive = true + Tags = [ "admin"; "user" ] + OptionName = Some "Alice" + VOptionId = ValueSome 1 + } + { + Id = 2 + Name = "Bob" + Status = Inactive + Color = Color.Green + GuidField = Guid.Parse "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + WrappedName = WrappedString "Bob" + WrappedScore = WrappedInt 20 + WrappedLong = WrappedInt64 200L + WrappedGuid = WrappedGuid (Guid.Parse "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") + CreatedAt = DateTime (2024, 2, 1) + BirthDate = DateOnly (1985, 8, 20) + AlarmTime = TimeOnly (9, 0) + Score = 200m + IsActive = false + Tags = [ "user" ] + OptionName = None + VOptionId = ValueNone + } + { + Id = 3 + Name = "Charlie" + Status = Pending + Color = Color.Blue + GuidField = Guid.Parse "cccccccc-cccc-cccc-cccc-cccccccccccc" + WrappedName = WrappedString "Charlie" + WrappedScore = WrappedInt 30 + WrappedLong = WrappedInt64 300L + WrappedGuid = WrappedGuid (Guid.Parse "cccccccc-cccc-cccc-cccc-cccccccccccc") + CreatedAt = DateTime (2024, 3, 1) + BirthDate = DateOnly (2000, 12, 31) + AlarmTime = TimeOnly (10, 0) + Score = 300m + IsActive = true + Tags = [ "moderator"; "user" ] + OptionName = Some "Charlie" + VOptionId = ValueSome 3 + } + |] + +let applyFilter (filter : ObjectListFilter) = + filter.ApplyTo (testData.AsQueryable (), filterOptions) |> Seq.toList diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionValueTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionValueTests.fs new file mode 100644 index 000000000..123d354c2 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionValueTests.fs @@ -0,0 +1,210 @@ +[] +module FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.ValueTests + +open Xunit +open System +open FSharp.Data.GraphQL.Server.Middleware +open FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.Common + +// ────────────────────────────────────────────────────────────────────────────── +// pass-through (value already has the target type) +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``tryCoerceValue passes through string`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box "hello") + |> wantValueSome |> equals (box "hello") + +[] +let ``tryCoerceValue passes through int`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box 42) + |> wantValueSome |> equals (box 42) + +[] +let ``tryCoerceValue passes through bool`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box true) + |> wantValueSome |> equals (box true) + +[] +let ``tryCoerceValue passes through Guid`` () = + let g = Guid.NewGuid () + TypeCoercion.tryCoerceValue noOptions typeof (box g) + |> wantValueSome |> equals (box g) + +[] +let ``tryCoerceValue passes through decimal`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box 9.99m) + |> wantValueSome |> equals (box 9.99m) + +[] +let ``tryCoerceValue passes through DateTime`` () = + let dt = DateTime (2024, 6, 1) + TypeCoercion.tryCoerceValue noOptions typeof (box dt) + |> wantValueSome |> equals (box dt) + +// ────────────────────────────────────────────────────────────────────────────── +// null → ValueNone +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``tryCoerceValue returns ValueNone for null`` () = + TypeCoercion.tryCoerceValue noOptions typeof null + |> wantValueNone + +// ────────────────────────────────────────────────────────────────────────────── +// numeric widening / narrowing +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``tryCoerceValue coerces int to int64`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box 42) + |> wantValueSome |> equals (box 42L) + +[] +let ``tryCoerceValue returns ValueNone for string-to-int (STJ rejects quoted number)`` () = + // STJ does not coerce quoted strings to numbers without JsonNumberHandling.AllowReadingFromString + TypeCoercion.tryCoerceValue noOptions typeof (box "99") + |> wantValueNone + +[] +let ``tryCoerceValue returns ValueNone for string-to-int64 (STJ rejects quoted number)`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box "123456789") + |> wantValueNone + +[] +let ``tryCoerceValue coerces double to decimal`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box 3.14) + |> wantValueSome |> equals (box 3.14m) + +[] +let ``tryCoerceValue coerces int to decimal`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box 200) + |> wantValueSome |> equals (box 200m) + +[] +let ``tryCoerceValue passes through bool (already correct type)`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box true) + |> wantValueSome |> equals (box true) + +// ────────────────────────────────────────────────────────────────────────────── +// string → Guid +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``tryCoerceValue coerces string to Guid`` () = + let g = Guid.Parse "550e8400-e29b-41d4-a716-446655440000" + TypeCoercion.tryCoerceValue noOptions typeof (box "550e8400-e29b-41d4-a716-446655440000") + |> wantValueSome |> equals (box g) + +[] +let ``tryCoerceValue returns ValueNone for invalid Guid string`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box "not-a-guid") + |> wantValueNone + +// ────────────────────────────────────────────────────────────────────────────── +// string → date/time types (native STJ support) +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``tryCoerceValue coerces ISO string to DateTime`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box "2024-06-01T00:00:00") + |> wantValueSome |> equals (box (DateTime (2024, 6, 1, 0, 0, 0))) + +[] +let ``tryCoerceValue coerces ISO string to DateTimeOffset`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box "2024-06-01T12:00:00+00:00") + |> wantValueSome |> ignore + +[] +let ``tryCoerceValue coerces ISO string to DateOnly`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box "2024-06-01") + |> wantValueSome |> equals (box (DateOnly (2024, 6, 1))) + +[] +let ``tryCoerceValue coerces ISO string to TimeOnly`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box "14:30:00") + |> wantValueSome |> equals (box (TimeOnly (14, 30, 0))) + +// ────────────────────────────────────────────────────────────────────────────── +// CLR enum +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``tryCoerceValue coerces int to CLR enum without jsonOptions`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box 2) + |> wantValueSome |> equals (box Color.Blue) + +[] +let ``tryCoerceValue coerces string to CLR enum with jsonOptions (JsonStringEnumConverter)`` () = + TypeCoercion.tryCoerceValue jsonOptions typeof (box "Green") + |> wantValueSome |> equals (box Color.Green) + +[] +let ``tryCoerceValue returns ValueNone for string CLR enum without jsonOptions`` () = + // Without JsonStringEnumConverter, STJ rejects string enum tokens by default + TypeCoercion.tryCoerceValue noOptions typeof (box "Red") + |> wantValueNone + +// ────────────────────────────────────────────────────────────────────────────── +// single-case DU (requires FSharp.SystemTextJson UnwrapSingleCaseUnions) +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``tryCoerceValue coerces string to single-case DU wrapping string`` () = + TypeCoercion.tryCoerceValue jsonOptions typeof (box "hello") + |> wantValueSome |> equals (box (WrappedString "hello")) + +[] +let ``tryCoerceValue coerces int to single-case DU wrapping int`` () = + TypeCoercion.tryCoerceValue jsonOptions typeof (box 42) + |> wantValueSome |> equals (box (WrappedInt 42)) + +[] +let ``tryCoerceValue coerces int64 to single-case DU wrapping int64`` () = + TypeCoercion.tryCoerceValue jsonOptions typeof (box 999L) + |> wantValueSome |> equals (box (WrappedInt64 999L)) + +[] +let ``tryCoerceValue coerces string to single-case DU wrapping Guid`` () = + let g = Guid.Parse "550e8400-e29b-41d4-a716-446655440000" + TypeCoercion.tryCoerceValue jsonOptions typeof (box "550e8400-e29b-41d4-a716-446655440000") + |> wantValueSome |> equals (box (WrappedGuid g)) + +[] +let ``tryCoerceValue returns ValueNone for single-case DU without jsonOptions`` () = + // Without FSharp.SystemTextJson, STJ doesn't know how to deserialize DUs + TypeCoercion.tryCoerceValue noOptions typeof (box "hello") + |> wantValueNone + +// ────────────────────────────────────────────────────────────────────────────── +// multi-case fieldless DU / DU-as-enum (requires FSharp.SystemTextJson UnwrapFieldlessTags) +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``tryCoerceValue coerces string to multi-case fieldless DU - Active`` () = + TypeCoercion.tryCoerceValue jsonOptions typeof (box "Active") + |> wantValueSome |> equals (box Active) + +[] +let ``tryCoerceValue coerces string to multi-case fieldless DU - Inactive`` () = + TypeCoercion.tryCoerceValue jsonOptions typeof (box "Inactive") + |> wantValueSome |> equals (box Inactive) + +[] +let ``tryCoerceValue coerces string to multi-case fieldless DU - Pending`` () = + TypeCoercion.tryCoerceValue jsonOptions typeof (box "Pending") + |> wantValueSome |> equals (box Pending) + +[] +let ``tryCoerceValue returns ValueNone for unknown multi-case DU case name`` () = + TypeCoercion.tryCoerceValue jsonOptions typeof (box "Unknown") + |> wantValueNone + +// ────────────────────────────────────────────────────────────────────────────── +// unsupported conversion +// ────────────────────────────────────────────────────────────────────────────── + +[] +let ``tryCoerceValue returns ValueNone when source type has no JSON representation`` () = + TypeCoercion.tryCoerceValue noOptions typeof (box (obj ())) + |> wantValueNone diff --git a/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs index dcad59739..409c0a5a7 100644 --- a/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs @@ -1,5 +1,6 @@ // The MIT License (MIT) // Copyright (c) 2016 Bazinga Technologies Inc +[] module FSharp.Data.GraphQL.Tests.LinqTests open Xunit diff --git a/tests/FSharp.Data.GraphQL.Tests/TestAttributes.fs b/tests/FSharp.Data.GraphQL.Tests/TestAttributes.fs index 4d3cdc491..90d8da7e7 100644 --- a/tests/FSharp.Data.GraphQL.Tests/TestAttributes.fs +++ b/tests/FSharp.Data.GraphQL.Tests/TestAttributes.fs @@ -20,3 +20,21 @@ type UseInvariantCultureAttribute() = override _.After (methodUnderTest) = CultureInfo.CurrentUICulture <- _originalUICulture CultureInfo.CurrentCulture <- _originalCulture + +namespace Tests + +module TraitType = + + [] + let Category = "Category" + + [] + let ObjectListFilterOperator = "ObjectListFilter operator" + +module TraitName = + + [] + let Linq = "LINQ" + + [] + let ObjectListFilter = "ObjectListFilter" From 454a9e0a26c519b5f04a03446eb95e4a4dc94da1 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 16 Jul 2026 20:30:47 +0200 Subject: [PATCH 14/32] Migrated to modern `InternalsVisibleTo` syntax --- ...harp.Data.GraphQL.Server.Middleware.fsproj | 4 +--- .../FSharp.Data.GraphQL.Server.fsproj | 19 ++++-------------- .../FSharp.Data.GraphQL.Shared.fsproj | 20 +++++-------------- 3 files changed, 10 insertions(+), 33 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj b/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj index 4838ed431..ca6740760 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj +++ b/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj @@ -15,9 +15,7 @@ - - <_Parameter1>FSharp.Data.GraphQL.Tests - + diff --git a/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj b/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj index d97cc68fd..c409a5d0e 100644 --- a/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj +++ b/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj @@ -14,21 +14,10 @@ - - <_Parameter1>FSharp.Data.GraphQL.Benchmarks - - - <_Parameter1>FSharp.Data.GraphQL.Tests - - - - - - <_Parameter1>FSharp.Data.GraphQL.Server.AspNetCore - - - <_Parameter1>FSharp.Data.GraphQL.Server.Middleware - + + + + diff --git a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj index a116e8ea8..f23b49b1f 100644 --- a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj +++ b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj @@ -14,21 +14,11 @@ - - <_Parameter1>FSharp.Data.GraphQL.Server - - - <_Parameter1>FSharp.Data.GraphQL.Server.Middleware - - - <_Parameter1>FSharp.Data.GraphQL.Client - - - <_Parameter1>FSharp.Data.GraphQL.Client.DesignTime - - - <_Parameter1>FSharp.Data.GraphQL.Tests - + + + + + From 8e3423735786a4e06f70bed62064184e9dbe3ca9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 17 Jul 2026 21:07:40 +0200 Subject: [PATCH 15/32] Implemented support of `Guid` and value object scalars as `InputValue` * Enhanced `InputValue.OfObject` to handle `Guid` (as `StringValue "D"`), `IReadOnlyDictionary`/`IDictionary` (as `ObjectValue`), and improved F# union handling. * Added `GuidId` DU, wrapped scalar, and new `Guid`/`ValueObject` fields to test types, extended tests for filtering with `Guid` and custom value object scalars. --- src/FSharp.Data.GraphQL.Shared/Ast.fs | 30 ++-- .../MiddlewareTests.fs | 150 ++++++++++++++++-- 2 files changed, 159 insertions(+), 21 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Shared/Ast.fs b/src/FSharp.Data.GraphQL.Shared/Ast.fs index 90b47455c..bd343d478 100644 --- a/src/FSharp.Data.GraphQL.Shared/Ast.fs +++ b/src/FSharp.Data.GraphQL.Shared/Ast.fs @@ -4,6 +4,7 @@ namespace FSharp.Data.GraphQL.Ast open System open System.Text.Json +open Microsoft.FSharp.Reflection //NOTE: For references, see https://facebook.github.io/graphql/ /// 2.2 Query Document @@ -124,26 +125,35 @@ and InputValue = | :? single as value -> FloatValue (double value) | :? bool as value -> BooleanValue value | :? string as value -> StringValue value + | :? Guid as value -> StringValue (value.ToString "D") | :? uint64 as value -> IntValue (int64 value) | :? uint32 as value -> IntValue (int64 value) | :? uint16 as value -> IntValue (int64 value) + | :? System.Collections.Generic.IReadOnlyDictionary as dict -> + let map = + dict + |> Seq.map (fun kv -> kv.Key, InputValue.OfObject kv.Value) + |> Map.ofSeq + ObjectValue map + | :? System.Collections.Generic.IDictionary as dict -> + let map = + dict + |> Seq.map (fun kv -> kv.Key, InputValue.OfObject kv.Value) + |> Map.ofSeq + ObjectValue map | value -> let ``type`` = value.GetType() if ``type``.IsArray then let array = value :?> System.Array let list = [ for i in 0 .. array.Length - 1 -> InputValue.OfObject (array.GetValue i) ] ListValue list + elif FSharpType.IsUnion (``type``, true) then + let _, unionFields = FSharpValue.GetUnionFields (value, ``type``, true) + match unionFields with + | [| singleField |] -> InputValue.OfObject singleField + | _ -> failwith "Cannot convert object to 'InputValue'" else - let genericType = ``type``.GetGenericTypeDefinition() - if typeof>.IsAssignableFrom genericType then - let dict = value :?> System.Collections.Generic.IReadOnlyDictionary - let map = - dict - |> Seq.map (fun kv -> kv.Key.ToString(), InputValue.OfObject kv.Value) - |> Map.ofSeq - ObjectValue map - else - failwith "Cannot convert object to 'InputValue'" + failwith "Cannot convert object to 'InputValue'" static member OfJsonElement (element : JsonElement) = match element.ValueKind with diff --git a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs index 0e177cae8..0584313d1 100644 --- a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs @@ -10,22 +10,65 @@ open FSharp open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Server.Middleware -open FSharp.Data.GraphQL.Shared open FSharp.Data.GraphQL.Parser -open FSharp.Data.GraphQL.Execution open FSharp.Data.GraphQL.Ast #nowarn "40" type Root = { clientId : int } -and Subject = +type GuidId = ValueObjectId of Guid + +let private parseGuidId (value : string) = + match Guid.TryParse value with + | true, guid -> Ok (ValueObjectId guid) + | false, _ -> + Error [ + { new IGQLError with + member _.Message = $"Cannot coerce '{value}' to GuidID" + } + ] + +let private guidIdToString (ValueObjectId guidId) = guidId.ToString "D" + +let ValueObjectType = + Define.WrappedScalar ( + name = "ValueObject", + coerceInput = + (function + | InputParameterValue.Variable value when value.ValueKind = JsonValueKind.String -> parseGuidId (value.GetString ()) + | InputParameterValue.InlineConstant (StringValue value) -> parseGuidId value + | _ -> + Error [ + { new IGQLError with + member _.Message = "ValueObject must be provided as string" + } + ]), + coerceOutput = + (function + | :? GuidId as guid -> guidIdToString guid |> Some + | _ -> None) + ) + +type Subject = | A of A | B of B -and A = { Id : int; Value : string; Subjects : int list } +and A = { + Id : int + Value : string + GuidValue : Guid + ValueObject : GuidId + Subjects : int list +} -and B = { Id : int; Value : string; Subjects : int list } +and B = { + Id : int + Value : string + GuidValue : Guid + ValueObject : GuidId + Subjects : int list +} type Complex = { Id : int @@ -43,12 +86,12 @@ type Property = | Community of Community let getExecutor (expectedFilter : ObjectListFilter voption) = - let a1 : A = { Id = 1; Value = "A1"; Subjects = [ 2; 6 ] } - let a2 : A = { Id = 2; Value = "A2"; Subjects = [ 1; 3; 5 ] } - let a3 : A = { Id = 3; Value = "A3"; Subjects = [ 1; 2; 4 ] } - let b1 = { Id = 4; Value = "1000"; Subjects = [ 1; 5 ] } - let b2 = { Id = 5; Value = "2000"; Subjects = [ 3; 4; 6 ] } - let b3 = { Id = 6; Value = "3000"; Subjects = [ 1; 3; 5 ] } + let a1 : A = { Id = 1; Value = "A1"; GuidValue = Guid.Parse "11111111-1111-1111-1111-111111111111"; ValueObject = ValueObjectId (Guid.Parse "11111111-1111-1111-1111-111111111111"); Subjects = [ 2; 6 ] } + let a2 : A = { Id = 2; Value = "A2"; GuidValue = Guid.Parse "22222222-2222-2222-2222-222222222222"; ValueObject = ValueObjectId (Guid.Parse "22222222-2222-2222-2222-222222222222"); Subjects = [ 1; 3; 5 ] } + let a3 : A = { Id = 3; Value = "A3"; GuidValue = Guid.Parse "33333333-3333-3333-3333-333333333333"; ValueObject = ValueObjectId (Guid.Parse "33333333-3333-3333-3333-333333333333"); Subjects = [ 1; 2; 4 ] } + let b1 = { Id = 4; Value = "1000"; GuidValue = Guid.Parse "44444444-4444-4444-4444-444444444444"; ValueObject = ValueObjectId (Guid.Parse "44444444-4444-4444-4444-444444444444"); Subjects = [ 1; 5 ] } + let b2 = { Id = 5; Value = "2000"; GuidValue = Guid.Parse "55555555-5555-5555-5555-555555555555"; ValueObject = ValueObjectId (Guid.Parse "55555555-5555-5555-5555-555555555555"); Subjects = [ 3; 4; 6 ] } + let b3 = { Id = 6; Value = "3000"; GuidValue = Guid.Parse "66666666-6666-6666-6666-666666666666"; ValueObject = ValueObjectId (Guid.Parse "66666666-6666-6666-6666-666666666666"); Subjects = [ 1; 3; 5 ] } let al = [ a1; a2; a3 ] let bl = [ b1; b2; b3 ] let p1 = Complex{ Id = 1; Name = "Complex 1"; Discriminator = "Complex"; Communities = [ 5 ]; Buildings = [ 3 ] } @@ -90,6 +133,8 @@ let getExecutor (expectedFilter : ObjectListFilter voption) = fun () -> [ Define.Field ("id", IntType, resolve = (fun _ a -> a.Id)) Define.Field ("value", StringType, resolve = (fun _ a -> a.Value)) + Define.Field ("guidValue", GuidType, resolve = (fun _ a -> a.GuidValue)) + Define.Field ("valueObject", ValueObjectType, resolve = (fun _ a -> a.ValueObject)) Define .Field( "subjects", @@ -111,6 +156,8 @@ let getExecutor (expectedFilter : ObjectListFilter voption) = fun () -> [ Define.Field ("id", IntType, resolve = (fun _ b -> b.Id)) Define.Field ("value", StringType, resolve = (fun _ b -> b.Value)) + Define.Field ("guidValue", GuidType, resolve = (fun _ b -> b.GuidValue)) + Define.Field ("valueObject", ValueObjectType, resolve = (fun _ b -> b.ValueObject)) Define .Field( "subjects", @@ -1093,6 +1140,87 @@ let ``Object list filter: Must parse filter that references variables`` () = data |> equals (upcast expected) result.Metadata.TryFind ("filters") |> wantValueSome |> seqEquals [ expectedFilter ] +[] +let ``Object list filter: Must parse inline filter variable backed by Guid scalar`` () = + let query = + parse + """query testQuery($filter: Guid!) { + A (id : 1) { + id + value + subjects (filter : { guidValue : $filter }) { ...Value } + } + } + + fragment Value on Subject { + ...on A { + id + value + } + ...on B { + id + value + } + }""" + let expected = + NameValueLookup.ofList [ + "A", + upcast + NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", + upcast + [ + NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] + NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ] + ] + ] + ] + + let guidText = "22222222-2222-2222-2222-222222222222" + let filterValue = $"\"{guidText}\"" |> JsonDocument.Parse |> _.RootElement + let variables = ImmutableDictionary.Empty.Add ("filter", filterValue) + let filter = Equals ({ FieldName = "guidvalue"; Value = guidText }, null) + let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) filter + let result = executeAndVerifyFilter (query, variables, filter) + + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + result.Metadata.TryFind ("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + +[] +let ``Object list filter: Must parse inline filter variable backed by wrapped value object`` () = + let query = + parse + """query testQuery($valueObject: ValueObject!) { + A (id : 1) { + id + value + subjects (filter : { valueObject : $valueObject }) { ...Value } + } + } + + fragment Value on Subject { + ...on A { + id + value + } + ...on B { + id + value + } + }""" + + let valueObjectText = "22222222-2222-2222-2222-222222222222" + let valueObjectVariable = $"\"{valueObjectText}\"" |> JsonDocument.Parse |> _.RootElement + let variables = ImmutableDictionary.Empty.Add ("valueObject", valueObjectVariable) + let result = executeWithVariables (query, variables) + + ensureDirect result <| fun _ errors -> + empty errors + [] let ``Object list filter: Must return empty filter when all discriminated union types are specified`` () = let query = From 78d21c1c3524a517ffe240849de84222067de15c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 9 Aug 2026 22:57:44 +0200 Subject: [PATCH 16/32] Implemented tests for `ObjectListFilter` with empty arrays * Added `ObjectListFilterEmptyArrayTests.fs` with cases for Contains, Equals, Not Equals, and logical operators on empty/non-empty lists. Updated `.fsproj` to include the new file. * Extended `ObjectListFilterLinqGenerateTests.fs` to verify correct Cosmos SQL generation for list equality and length checks. --- .../FSharp.Data.GraphQL.Tests.fsproj | 1 + .../ObjectListFilterEmptyArrayTests.fs | 220 ++++++++++++++++++ .../ObjectListFilterLinqGenerateTests.fs | 45 ++++ 3 files changed, 266 insertions(+) create mode 100644 tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterEmptyArrayTests.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 7242322a2..557c67428 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -83,6 +83,7 @@ + diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterEmptyArrayTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterEmptyArrayTests.fs new file mode 100644 index 000000000..097d61092 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterEmptyArrayTests.fs @@ -0,0 +1,220 @@ +[] +[] +module FSharp.Data.GraphQL.Tests.ObjectListFilter.EmptyArray.Tests + +open Xunit +open System +open System.Linq +open System.Text.Json +open FSharp.Data.GraphQL.Shared +open FSharp.Data.GraphQL.Server.Middleware + +// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── +// Test types +// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +type EmptyArrayEntity = { + Id: int + Name: string + Tags: string list +} + +// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── +// Test data +// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +let testData = [| + { Id = 1; Name = "Alice"; Tags = [] } + { Id = 2; Name = "Bob"; Tags = [ "admin"; "user" ] } + { Id = 3; Name = "Charlie"; Tags = [ "user" ] } + { Id = 4; Name = "Diana"; Tags = [] } +|] + +let filterOptions = ObjectListFilterLinqOptions (Json.getSerializerOptions Seq.empty) + +let applyFilter (filter : ObjectListFilter) = + filter.ApplyTo (testData.AsQueryable (), filterOptions) |> Seq.toList + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Validation Tests – Check that empty filters do NOT crash and handle correctly +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +[] +let ``Contains on empty array field returns entities with empty tags`` () = + // Contains on an empty list should match entities with empty lists + let filter = Contains ({ FieldName = "tags"; Value = "admin" }, null) + let result = applyFilter filter + // Only Bob and Charlie have "admin" or any tags + result |> List.length |> equals 1 + result |> List.map (fun e -> e.Name) |> equals [ "Bob" ] + +[] +let ``Empty filter list returns all entities`` () = + // No filter applied means all entities pass + // The built-in queryable should return all + let result = testData.AsQueryable () |> Seq.toList + result |> List.length |> equals 4 + +[] +let ``And with empty field matches correctly`` () = + // Filter: (id > 1 AND tags contains "admin") + let filter = + And ( + GreaterThan { FieldName = "id"; Value = 1 }, + Contains ({ FieldName = "tags"; Value = "admin" }, null) + ) + let result = applyFilter filter + // Only Bob (id=2) has "admin" tag and id > 1 + result |> List.length |> equals 1 + (List.head result).Name |> equals "Bob" + +[] +let ``Or with empty field returns union of results`` () = + // Filter: (id = 1 OR tags contains "admin") + let filter = + Or ( + Equals ({ FieldName = "id"; Value = 1 }, null), + Contains ({ FieldName = "tags"; Value = "admin" }, null) + ) + let result = applyFilter filter + // Alice (id=1) and Bob (has "admin") + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Bob" ] + +[] +let ``Not on empty array field returns complementary set`` () = + // Filter: NOT (tags contains "user") + let filter = Not (Contains ({ FieldName = "tags"; Value = "user" }, null)) + let result = applyFilter filter + // Alice, Diana have empty tags; Bob and Charlie have "user" + // so NOT "user" = Alice, Diana + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Diana" ] + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// SQL Generation Tests – Check that LINQ expression tree is correctly built +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +[] +let ``Contains filter on array field generates valid LINQ expression`` () = + // Verify the filter doesn't crash and produces a valid LINQ provider execution + // by actually running it + let filter = Contains ({ FieldName = "tags"; Value = "user" }, null) + let result = applyFilter filter + // Bob and Charlie have "user" tag + result |> List.length |> equals 2 + +[] +let ``Complex nested filter generates valid LINQ expression`` () = + // Nested: (NOT (id < 3)) AND (tags contains "user") + let filter = + And ( + Not (LessThan { FieldName = "id"; Value = 3 }), + Contains ({ FieldName = "tags"; Value = "user" }, null) + ) + let result = applyFilter filter + // id >= 3: Charlie (3), Diana (4) + // tags contains "user": Bob (2), Charlie (3) + // intersection: Charlie (3) + result |> List.length |> equals 1 + (List.head result).Name |> equals "Charlie" + +[] +let ``Equals empty list filter generates valid LINQ expression`` () = + // Verify: tags = [] + // Should correctly compile LINQ expression for list equality check + let emptyListValue = [] : string list + let filter = Equals ({ FieldName = "tags"; Value = emptyListValue }, null) + let result = applyFilter filter + // Alice and Diana have empty tags + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Diana" ] + +[] +let ``Not Equals empty list filter generates valid LINQ expression`` () = + // Verify: NOT (tags = []) => tags != [] + // Should correctly compile LINQ expression for list inequality check + let emptyListValue = [] : string list + let filter = Not (Equals ({ FieldName = "tags"; Value = emptyListValue }, null)) + let result = applyFilter filter + // Bob and Charlie have non-empty tags + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Bob"; "Charlie" ] + +[] +let ``Complex Equals empty list with logical operators generates valid LINQ expression`` () = + // Verify: (id <= 2) AND (tags = []) + let emptyListValue = [] : string list + let filter = + And ( + LessThanOrEqual { FieldName = "id"; Value = 2 }, + Equals ({ FieldName = "tags"; Value = emptyListValue }, null) + ) + let result = applyFilter filter + // id <= 2: Alice (1), Bob (2) + // tags = []: Alice (1), Diana (4) + // intersection: Alice (1) + result |> List.length |> equals 1 + (List.head result).Name |> equals "Alice" + +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +// Coercion Tests – Check that string values are correctly coerced to list membership checks +// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + +[] +let ``Contains coerces string value into list element check`` () = + // "admin" (string) should match list containing "admin" + let filter = Contains ({ FieldName = "tags"; Value = "admin" }, null) + let result = applyFilter filter + result |> List.length |> equals 1 + result |> List.map (fun e -> e.Name) |> equals [ "Bob" ] + +[] +let ``Contains returns empty result when no match`` () = + // Looking for a tag that doesn't exist in any entity + let filter = Contains ({ FieldName = "tags"; Value = "superadmin" }, null) + let result = applyFilter filter + result |> List.length |> equals 0 + +[] +let ``Contains with multiple identical tags matches correctly`` () = + // If an entity had ["user"; "user"], Contains "user" should still match + // (list element membership check, not count) + let filter = Contains ({ FieldName = "tags"; Value = "user" }, null) + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Bob"; "Charlie" ] + +[] +let ``Equals on list field with empty list returns only empty lists`` () = + // Filter: tags = [] + let emptyListValue = [] : string list + let filter = Equals ({ FieldName = "tags"; Value = emptyListValue }, null) + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Diana" ] + +[] +let ``Not Equals on empty list returns only non-empty lists`` () = + // Filter: NOT (tags = []) => tags != [] => only entities with non-empty tags + let emptyListValue = [] : string list + let filter = Not (Equals ({ FieldName = "tags"; Value = emptyListValue }, null)) + let result = applyFilter filter + result |> List.length |> equals 2 + result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Bob"; "Charlie" ] + +[] +let ``Implicit non-empty check via Contains finds all entities with any tag`` () = + // Any entity where Contains matches ANY tag is considered "has tags" + // This is implicit non-empty: if Contains("user") or Contains("admin") matches, the list is non-empty + let filter1 = Contains ({ FieldName = "tags"; Value = "user" }, null) + let filter2 = Contains ({ FieldName = "tags"; Value = "admin" }, null) + let result1 = applyFilter filter1 // "user": Bob, Charlie + let result2 = applyFilter filter2 // "admin": Bob + let combined = + (result1 |> List.map (fun e -> e.Id)) + @ (result2 |> List.map (fun e -> e.Id)) + |> List.distinct + |> List.sort + // Bob (2) and Charlie (3) have at least one tag + combined |> equals [ 2; 3 ] diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqGenerateTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqGenerateTests.fs index 27d5332d4..c1033fcea 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqGenerateTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqGenerateTests.fs @@ -270,6 +270,51 @@ let ``ObjectListFilter works with In operator for empty ValidStringStruct list`` let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE false""" +[] +let ``ObjectListFilter works with Equals operator for empty ValidStringStructList`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Equals ({ FieldName = "validStringStructList"; Value = [] }, null) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + // Equals empty list should use ARRAY_LENGTH = 0 or NOT ANY + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (ARRAY_LENGTH(root["validStringStructList"]) = 0)""" + +[] +let ``ObjectListFilter works with Not Equals operator for empty ValidStringStructList`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Not (Equals ({ FieldName = "validStringStructList"; Value = [] }, null)) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + // Not Equals empty list should use ARRAY_LENGTH > 0 + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (ARRAY_LENGTH(root["validStringStructList"]) > 0)""" + +[] +let ``ObjectListFilter works with Equals operator for non-empty ValidStringStructList`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Equals ({ FieldName = "validStringStructList"; Value = ["tag1"; "tag2"] }, null) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + // Equals non-empty list should check exact list match + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validStringStructList"] = ["tag1", "tag2"])""" + +[] +let ``ObjectListFilter works with Not Equals operator for non-empty ValidStringStructList`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Not (Equals ({ FieldName = "validStringStructList"; Value = ["tag1"; "tag2"] }, null)) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + // Not Equals non-empty list should check NOT exact list match + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validStringStructList"] != ["tag1", "tag2"])""" + +[] +let ``ObjectListFilter works with Equals operator for single-element ValidStringStructList`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Equals ({ FieldName = "validStringStructList"; Value = ["tag1"] }, null) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + // Equals single-element list + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validStringStructList"] = ["tag1"])""" + [] let ``ObjectListFilter works with Equals operator for ValidStringObject`` () = let filter = Equals ({ FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" }, null) From 060fd26180a3048293c8f2e8245fb52260a3ca2c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 9 Aug 2026 22:59:02 +0200 Subject: [PATCH 17/32] Updated SDK to `10.0.302` --- .github/workflows/publish-ci.yml | 2 +- .github/workflows/publish-release.yml | 2 +- .github/workflows/pull-request.yml | 2 +- build/Program.fs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-ci.yml b/.github/workflows/publish-ci.yml index 3b26ec16a..73c714daa 100644 --- a/.github/workflows/publish-ci.yml +++ b/.github/workflows/publish-ci.yml @@ -8,7 +8,7 @@ on: env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true - DOTNET_SDK_VERSION: 10.0.301 + DOTNET_SDK_VERSION: 10.0.302 jobs: publish: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index c329f6816..ae0941cfc 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -9,7 +9,7 @@ env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true SLEEP_DURATION: 60 - DOTNET_SDK_VERSION: 10.0.301 + DOTNET_SDK_VERSION: 10.0.302 jobs: publish: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 228084b60..685828262 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -38,7 +38,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-22.04, windows-latest, macOS-latest] - dotnet: [10.0.301] + dotnet: [10.0.302] runs-on: ${{ matrix.os }} steps: diff --git a/build/Program.fs b/build/Program.fs index b5b73573d..a269caf26 100644 --- a/build/Program.fs +++ b/build/Program.fs @@ -30,7 +30,7 @@ let ctx = Context.forceFakeContext () let embedAll = ctx.Arguments |> List.exists (fun arg -> arg = BuildArguments.EmbedAll) module DotNetCli = - let setVersion (o : DotNet.Options) = { o with Version = Some "10.0.301" } + let setVersion (o : DotNet.Options) = { o with Version = Some "10.0.302" } let setRestoreOptions (o : DotNet.RestoreOptions) = o.WithCommon setVersion let configurationString = Environment.environVarOrDefault "CONFIGURATION" "Release" From d971701b943109c70a64cda62de321077451af26 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 9 Aug 2026 22:59:55 +0200 Subject: [PATCH 18/32] Updated `Fantomas` to `7.0.5` --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 17de00296..e4b18c908 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "7.0.1", + "version": "7.0.5", "commands": [ "fantomas" ], From eada5c2662aeffa8c06cf38c2d0ea16515100dfe Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 9 Aug 2026 23:01:00 +0200 Subject: [PATCH 19/32] Updated `FSDocs-Tools` to `22.1.0` --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index e4b18c908..26363e2ef 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "fsdocs-tool": { - "version": "20.0.1", + "version": "22.1.0", "commands": [ "fsdocs" ], From b9ce3ad3b85eab7f4f1e94e8d07a05b5004d8d76 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 10 Aug 2026 00:53:43 +0200 Subject: [PATCH 20/32] Implemented robust interface field covariance validation & tests - Implemented comprehensive validation for interface field covariance, covering output type subtyping, argument compatibility, and field implementation checks. - Enhanced error messages with clearer formatting and context. - Updated validation logic for object, input object, union, and enum types to use interpolated strings. - Added `InterfaceCovarianceTests.fs` with extensive valid/invalid covariance scenarios. - Modernized `TypeValidationTests.fs` and expanded `UnionInterfaceTests.fs` for execution coverage. --- src/FSharp.Data.GraphQL.Shared/Validation.fs | 76 +++- .../FSharp.Data.GraphQL.Tests.fsproj | 3 +- .../InterfaceCovarianceTests.fs | 419 ++++++++++++++++++ .../TypeValidationTests.fs | 78 ++-- .../UnionInterfaceTests.fs | 133 ++++++ 5 files changed, 671 insertions(+), 38 deletions(-) create mode 100644 tests/FSharp.Data.GraphQL.Tests/InterfaceCovarianceTests.fs diff --git a/src/FSharp.Data.GraphQL.Shared/Validation.fs b/src/FSharp.Data.GraphQL.Shared/Validation.fs index cf795c677..a0d45ef1c 100644 --- a/src/FSharp.Data.GraphQL.Shared/Validation.fs +++ b/src/FSharp.Data.GraphQL.Shared/Validation.fs @@ -15,6 +15,65 @@ open FsToolkit.ErrorHandling module Types = + let private asOutputDef (tdef : TypeDef) = + match tdef with + | :? OutputDef as output -> ValueSome output + | _ -> ValueNone + + let private isOptionalInputField (field : InputFieldDef) = + match field.TypeDef with + | Nullable _ -> true + | _ -> field.DefaultValue.IsSome || field.IsSkippable + + let private areFieldArgumentsCompatible (objArgs : InputFieldDef[]) (ifaceArgs : InputFieldDef[]) = + let objectArguments = objArgs |> Array.map (fun arg -> arg.Name, arg) |> Map.ofArray + let interfaceArguments = ifaceArgs |> Array.map (fun arg -> arg.Name, arg) |> Map.ofArray + + let hasCompatibleInterfaceArguments = + ifaceArgs + |> Array.forall (fun ifaceArg -> + match Map.tryFind ifaceArg.Name objectArguments with + | Some objArg -> objArg.TypeDef = ifaceArg.TypeDef + | None -> false) + + let hasOptionalExtraObjectArguments = + objArgs + |> Array.forall (fun objArg -> + match Map.tryFind objArg.Name interfaceArguments with + | Some ifaceArg -> objArg.TypeDef = ifaceArg.TypeDef + | None -> isOptionalInputField objArg) + + hasCompatibleInterfaceArguments && hasOptionalExtraObjectArguments + + let rec private isOutputSubtype (objType : OutputDef) (ifaceType : OutputDef) = + match objType, ifaceType with + | Nullable objInner, Nullable ifaceInner -> + match asOutputDef objInner, asOutputDef ifaceInner with + | ValueSome objOutput, ValueSome ifaceOutput -> isOutputSubtype objOutput ifaceOutput + | _ -> false + | Nullable _, _ -> false + | _, Nullable ifaceInner -> + match asOutputDef ifaceInner with + | ValueSome ifaceOutput -> isOutputSubtype objType ifaceOutput + | _ -> false + | List objInner, List ifaceInner -> + match asOutputDef objInner, asOutputDef ifaceInner with + | ValueSome objOutput, ValueSome ifaceOutput -> isOutputSubtype objOutput ifaceOutput + | _ -> false + | List _, _ + | _, List _ -> false + | _ when objType = ifaceType -> true + | (:? ObjectDef as objObject), (:? InterfaceDef as ifaceInterface) -> + objObject.Implements |> Array.exists ((=) ifaceInterface) + | (:? ObjectDef as objObject), (:? UnionDef as ifaceUnion) -> + ifaceUnion.Options |> Array.exists ((=) objObject) + | _ -> false + + let private isFieldImplementationCompatible (objField : FieldDef) (ifaceField : FieldDef) = + objField.Name = ifaceField.Name + && areFieldArgumentsCompatible objField.Args ifaceField.Args + && isOutputSubtype objField.TypeDef ifaceField.TypeDef + let validateImplements (objdef : ObjectDef) (idef : InterfaceDef) = let objectFields = objdef.Fields let errors = @@ -23,11 +82,11 @@ module Types = (fun acc f -> match Map.tryFind f.Name objectFields with | None -> - $"'%s{f.Name}' field is defined by interface %s{idef.Name}, but not implemented in object %s{objdef.Name}" + $"'%s{f.Name}' field is defined by interface '%s{idef.Name}', but not implemented in object '%s{objdef.Name}'" :: acc - | Some objf when objf = f -> acc + | Some objf when isFieldImplementationCompatible objf f -> acc | Some _ -> - $"'%s{objdef.Name}.%s{f.Name}' field signature does not match it's definition in interface %s{idef.Name}" + $"'%s{objdef.Name}.%s{f.Name}' field signature does not match it's definition in interface '%s{idef.Name}'" :: acc) [] match errors with @@ -42,7 +101,7 @@ module Types = if objdef.Fields.Count > 0 then Success else - ValidationError [ objdef.Name + " must have at least one field defined" ] + ValidationError [ $"'%s{objdef.Name}' must have at least one field defined" ] let implementsResult = objdef.Implements |> ValidationResult.collect (validateImplements objdef) @@ -52,7 +111,7 @@ module Types = if indef.Fields.Length > 0 then Success else - ValidationError [ indef.Name + " must have at least one field defined" ] + ValidationError [ $"'%s{indef.Name}' must have at least one field defined" ] nonEmptyResult | Union uniondef -> let nonEmptyResult = @@ -60,8 +119,7 @@ module Types = Success else ValidationError [ - uniondef.Name - + " must have at least one type definition option" + $"'%s{uniondef.Name}' must have at least one type definition option" ] nonEmptyResult | Enum enumdef -> @@ -69,14 +127,14 @@ module Types = if enumdef.Options.Length > 0 then Success else - ValidationError [ enumdef.Name + " must have at least one enum value defined" ] + ValidationError [ $"'%s{enumdef.Name}' must have at least one enum value defined" ] nonEmptyResult | Interface idef -> let nonEmptyResult = if idef.Fields.Length > 0 then Success else - ValidationError [ idef.Name + " must have at least one field defined" ] + ValidationError [ $"'%s{idef.Name}' must have at least one field defined" ] nonEmptyResult | InputCustom _ -> Success | _ -> failwithf "Unexpected value of typedef: %O" typedef 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 557c67428..9615ba1a9 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -38,9 +38,10 @@ + + - diff --git a/tests/FSharp.Data.GraphQL.Tests/InterfaceCovarianceTests.fs b/tests/FSharp.Data.GraphQL.Tests/InterfaceCovarianceTests.fs new file mode 100644 index 000000000..8c7f9c797 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/InterfaceCovarianceTests.fs @@ -0,0 +1,419 @@ +// The MIT License (MIT) +// Copyright (c) 2016 Bazinga Technologies Inc +module FSharp.Data.GraphQL.Tests.InterfaceCovarianceTests + +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Validation +open FSharp.Data.GraphQL.Validation.Types +open FSharp.Data.GraphQL.Types +open Helpers +open Xunit + +type IChildView = + interface + abstract Id : string + end + +type IParentView = + interface + abstract Child : IChildView + end + +type ChildAView = { + Id : string +} with + + interface IChildView with + member x.Id = x.Id + +type ChildBView = { + Id : string +} with + + interface IChildView with + member x.Id = x.Id + +type OtherView = { Name : string } + +type ParentAInfoView = { + Child : ChildAView +} with + + interface IParentView with + member x.Child = x.Child :> IChildView + +type ParentBInfoView = { + Child : ChildBView +} with + + interface IParentView with + member x.Child = x.Child :> IChildView + +type ParentOtherInfoView = { Child : OtherView } + +type ParentInterfaceChildView = { + Child : IChildView +} with + + interface IParentView with + member x.Child = x.Child + +type ParentChildOptionView = { Child : ChildAView option } + +type ParentChildVOptionView = { Child : ChildAView voption } + +type ParentChildListView = { Child : ChildAView list } + +type ParentChildOptionListView = { Child : ChildAView option list } + +type ParentChildVOptionListView = { Child : ChildAView voption list } + +let IChildInfo : InterfaceDef = + Define.Interface (name = "IChildInfo", fields = [ Define.Field ("id", StringType) ]) + +let ChildAInfo : ObjectDef = + Define.Object ( + name = "ChildAInfo", + fields = [ Define.Field ("id", StringType, (fun _ (x : ChildAView) -> x.Id)) ], + interfaces = [ IChildInfo ] + ) + +let ChildBInfo : ObjectDef = + Define.Object ( + name = "ChildBInfo", + fields = [ Define.Field ("id", StringType, (fun _ (x : ChildBView) -> x.Id)) ], + interfaces = [ IChildInfo ] + ) + +let OtherInfo : ObjectDef = + Define.Object (name = "OtherInfo", fields = [ Define.Field ("name", StringType, (fun _ x -> x.Name)) ]) + +let signatureMismatch objectName fieldName interfaceName = + $"'{objectName}.{fieldName}' field signature does not match it's definition in interface '{interfaceName}'" + +let hasValidationErrorContaining (text : string) result = + match result with + | ValidationError errors -> + Assert.True ( + (errors : string list) + |> List.exists (fun (x : string) -> x.Contains (text)), + $"Expected validation error containing '{text}', but got {errors}" + ) + | Success -> Assert.Fail ($"Expected validation error containing '{text}', but validation succeeded") + +[] +let ``Validation allows interface field covariance with ChildAInfo`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", ChildAInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + equals Success result + +[] +let ``Validation allows interface field covariance with ChildBInfo`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentBInfo", + fields = [ Define.Field ("child", ChildBInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + equals Success result + +[] +let ``Validation allows nullable interface field with non-null ChildAInfo`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", Nullable IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", ChildAInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + equals Success result + +[] +let ``Validation allows struct nullable interface field with non-null ChildAInfo`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", StructNullable IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", ChildAInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + equals Success result + +[] +let ``Validation allows exact non-null interface field type`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", IChildInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + equals Success result + +[] +let ``Validation allows list covariance from IChildInfo to ChildAInfo`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", ListOf IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", ListOf ChildAInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + equals Success result + +[] +let ``Validation allows deep wrapper covariance from Nullable(List(Nullable(IChildInfo))) to List(ChildAInfo)`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", Nullable (ListOf (Nullable IChildInfo))) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", ListOf ChildAInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + equals Success result + +[] +let ``Validation rejects unrelated object type for interface field`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", OtherInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + hasValidationErrorContaining (signatureMismatch "ParentAInfo" "child" "IParentInfo") result + +[] +let ``Validation rejects nullable object field when interface field is non-null`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", Nullable ChildAInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + hasValidationErrorContaining (signatureMismatch "ParentAInfo" "child" "IParentInfo") result + +[] +let ``Validation rejects struct nullable object field when interface field is non-null`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", StructNullable ChildAInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + hasValidationErrorContaining (signatureMismatch "ParentAInfo" "child" "IParentInfo") result + +[] +let ``Validation rejects scalar type when interface expects list`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", ListOf IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", ChildAInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + hasValidationErrorContaining (signatureMismatch "ParentAInfo" "child" "IParentInfo") result + +[] +let ``Validation rejects list item nullability widening with Nullable`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", ListOf IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", ListOf (Nullable ChildAInfo), (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + hasValidationErrorContaining (signatureMismatch "ParentAInfo" "child" "IParentInfo") result + +[] +let ``Validation rejects list item nullability widening with StructNullable`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", ListOf IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", ListOf (StructNullable ChildAInfo), (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + hasValidationErrorContaining (signatureMismatch "ParentAInfo" "child" "IParentInfo") result + +[] +let ``Validation rejects field arguments when required interface argument is missing`` () = + let parentInterface = + Define.Interface ( + name = "IParentInfo", + fields = [ + Define.Field ("child", IChildInfo, "Child field", [ Define.Input ("id", IntType) ], (fun _ _ -> Unchecked.defaultof)) + ] + ) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", ChildAInfo, "Child field", [], (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + hasValidationErrorContaining (signatureMismatch "ParentAInfo" "child" "IParentInfo") result + +[] +let ``Validation rejects field arguments when argument type differs`` () = + let parentInterface = + Define.Interface ( + name = "IParentInfo", + fields = [ + Define.Field ("child", IChildInfo, "Child field", [ Define.Input ("id", IntType) ], (fun _ _ -> Unchecked.defaultof)) + ] + ) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ + Define.Field ( + "child", + ChildAInfo, + "Child field", + [ Define.Input ("id", StringType) ], + (fun _ _ -> Unchecked.defaultof) + ) + ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + hasValidationErrorContaining (signatureMismatch "ParentAInfo" "child" "IParentInfo") result + +[] +let ``Validation rejects field arguments when object adds extra required argument`` () = + let parentInterface = + Define.Interface ( + name = "IParentInfo", + fields = [ + Define.Field ("child", IChildInfo, "Child field", [ Define.Input ("id", IntType) ], (fun _ _ -> Unchecked.defaultof)) + ] + ) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ + Define.Field ( + "child", + ChildAInfo, + "Child field", + [ Define.Input ("id", IntType); Define.Input ("extra", IntType) ], + (fun _ _ -> Unchecked.defaultof) + ) + ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + hasValidationErrorContaining (signatureMismatch "ParentAInfo" "child" "IParentInfo") result + +[] +let ``Validation allows field arguments when object adds extra optional argument`` () = + let parentInterface = + Define.Interface ( + name = "IParentInfo", + fields = [ + Define.Field ("child", IChildInfo, "Child field", [ Define.Input ("id", IntType) ], (fun _ _ -> Unchecked.defaultof)) + ] + ) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ + Define.Field ( + "child", + ChildAInfo, + "Child field", + [ Define.Input ("id", IntType); Define.Input ("extra", Nullable IntType) ], + (fun _ _ -> Unchecked.defaultof) + ) + ], + interfaces = [ parentInterface ] + ) + + let result = validateImplements parentObject parentInterface + equals Success result + +[] +let ``Validation type map allows covariance for concrete implementation of interface field`` () = + let parentInterface = + Define.Interface (name = "IParentInfo", fields = [ Define.Field ("child", IChildInfo) ]) + + let parentObject = + Define.Object ( + name = "ParentAInfo", + fields = [ Define.Field ("child", ChildAInfo, (fun _ _ -> Unchecked.defaultof)) ], + interfaces = [ parentInterface ] + ) + + let typeMap = TypeMap () + typeMap.AddType (parentObject) + + let result = validateTypeMap typeMap + equals Success result diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeValidationTests.fs b/tests/FSharp.Data.GraphQL.Tests/TypeValidationTests.fs index 70b5f394c..2afddea11 100644 --- a/tests/FSharp.Data.GraphQL.Tests/TypeValidationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/TypeValidationTests.fs @@ -14,54 +14,76 @@ type ITestInterface = abstract TestMethod : int -> string -> string end -type TestDataType = - { TestProperty : string } +type TestDataType = { + TestProperty : string +} with + interface ITestInterface with member x.TestProperty = x.TestProperty - member x.TestMethod i y = x.TestProperty + y + i.ToString() + member x.TestMethod i y = x.TestProperty + y + i.ToString () let TestInterface = - Define.Interface("TestInterface", - [ Define.Field("property", StringType) - Define.Field("method", StringType, "Test method", - [ Define.Input("x", IntType) - Define.Input("y", StringType) ], (fun _ _ -> "")) ]) + Define.Interface ( + "TestInterface", + [ + Define.Field ("property", StringType) + Define.Field ("method", StringType, "Test method", [ Define.Input ("x", IntType); Define.Input ("y", StringType) ], (fun _ _ -> "")) + ] + ) [] -let ``Validation must inform about not implemented fields``() = +let ``Validation must inform about not implemented fields`` () = let TestData = - Define.Object - (name = "TestData", fields = [ Define.Field("property", StringType, (fun _ d -> d.TestProperty)) ], - interfaces = [ TestInterface ]) + Define.Object ( + name = "TestData", + fields = [ Define.Field ("property", StringType, (fun _ d -> d.TestProperty)) ], + interfaces = [ TestInterface ] + ) let expected = - ValidationError [ "'method' field is defined by interface TestInterface, but not implemented in object TestData" ] + ValidationError [ "'method' field is defined by interface 'TestInterface', but not implemented in object 'TestData'" ] let result = validateImplements TestData TestInterface equals expected result [] -let ``Validation must inform about fields with not matching signatures``() = +let ``Validation must inform about fields with not matching signatures`` () = let TestData = - Define.Object - (name = "TestData", - fields = [ Define.Field("property", IntType, (fun _ d -> 1)) - Define.Field("method", StringType, "Test method", [ Define.Input("x", IntType) ], (fun _ _ -> "res")) ], - interfaces = [ TestInterface ]) + Define.Object ( + name = "TestData", + fields = [ + Define.Field ("property", IntType, (fun _ d -> 1)) + Define.Field ("method", StringType, "Test method", [ Define.Input ("x", IntType) ], (fun _ _ -> "res")) + ], + interfaces = [ TestInterface ] + ) let expected = - ValidationError - [ "'TestData.method' field signature does not match it's definition in interface TestInterface"; - "'TestData.property' field signature does not match it's definition in interface TestInterface" ] + ValidationError [ + "'TestData.method' field signature does not match it's definition in interface 'TestInterface'" + "'TestData.property' field signature does not match it's definition in interface 'TestInterface'" + ] + let result = validateImplements TestData TestInterface equals expected result [] -let ``Validation must succeed if object implements interface correctly``() = +let ``Validation must succeed if object implements interface correctly`` () = let TestData = - Define.Object - (name = "TestData", - fields = [ Define.Field("property", StringType, (fun _ d -> d.TestProperty)) - Define.Field("method", StringType, "Test method", [ Define.Input("x", IntType); Define.Input("y", StringType) ], (fun _ _ -> "res")) ], - interfaces = [ TestInterface ]) + Define.Object ( + name = "TestData", + fields = [ + Define.Field ("property", StringType, (fun _ d -> d.TestProperty)) + Define.Field ( + "method", + StringType, + "Test method", + [ Define.Input ("x", IntType); Define.Input ("y", StringType) ], + (fun _ _ -> "res") + ) + ], + interfaces = [ TestInterface ] + ) let result = validateImplements TestData TestInterface equals Success result + + diff --git a/tests/FSharp.Data.GraphQL.Tests/UnionInterfaceTests.fs b/tests/FSharp.Data.GraphQL.Tests/UnionInterfaceTests.fs index 291a8bf40..755b159f8 100644 --- a/tests/FSharp.Data.GraphQL.Tests/UnionInterfaceTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/UnionInterfaceTests.fs @@ -35,6 +35,16 @@ type Person = interface INamed with member x.Name = x.Name +type IHasChild = + interface + abstract Child : INamed + end + +type ParentWithDog = + { Child : Dog } + interface IHasChild with + member x.Child = x.Child :> INamed + let NamedType = Define.Interface( name = "Named", @@ -329,3 +339,126 @@ let ``Execute allows fragment conditions to be abstract types`` () = ensureDirect result <| fun data errors -> empty errors data |> equals (upcast expected) + +[] +let ``Executes covariance for interface field implemented by concrete object field`` () = + let hasChildType = + Define.Interface( + name = "HasChild", + fields = [ Define.Field("child", NamedType, fun _ (x : IHasChild) -> x.Child) ]) + + let parentWithDogType = + Define.Object( + name = "ParentWithDog", + isTypeOf = is, + interfaces = [ hasChildType ], + fields = [ Define.Field("child", DogType, fun _ x -> x.Child) ]) + + let queryType = + Define.Object( + name = "Query", + fields = [ Define.Field("parent", hasChildType, fun _ _ -> ({ Child = odie } :> IHasChild)) ]) + + let covariantSchema = + Schema(query = queryType, config = { SchemaConfig.Default with Types = [ parentWithDogType :> NamedDef ] }) + + let ast = parse """{ parent { __typename child { __typename name ... on Dog { barks } } } }""" + let result = sync <| Executor(covariantSchema).AsyncExecute(ast, getMockInputContext) + + let expected = + NameValueLookup.ofList [ + "parent", upcast NameValueLookup.ofList [ + "__typename", box "ParentWithDog" + "child", upcast NameValueLookup.ofList [ + "__typename", box "Dog" + "name", upcast "Odie" + "barks", upcast true + ] + ] + ] + + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + +[] +let ``Executes covariance for Nullable interface field implemented by non-null object field`` () = + let hasChildType = + Define.Interface( + name = "HasChildNullable", + fields = [ Define.Field("child", Nullable NamedType, fun _ (x : IHasChild) -> Some x.Child) ]) + + let parentWithDogType = + Define.Object( + name = "ParentWithDogNullable", + isTypeOf = is, + interfaces = [ hasChildType ], + fields = [ Define.Field("child", DogType, fun _ x -> x.Child) ]) + + let queryType = + Define.Object( + name = "QueryNullable", + fields = [ Define.Field("parent", hasChildType, fun _ _ -> ({ Child = odie } :> IHasChild)) ]) + + let covariantSchema = + Schema(query = queryType, config = { SchemaConfig.Default with Types = [ parentWithDogType :> NamedDef ] }) + + let ast = parse """{ parent { __typename child { __typename name ... on Dog { barks } } } }""" + let result = sync <| Executor(covariantSchema).AsyncExecute(ast, getMockInputContext) + + let expected = + NameValueLookup.ofList [ + "parent", upcast NameValueLookup.ofList [ + "__typename", box "ParentWithDogNullable" + "child", upcast NameValueLookup.ofList [ + "__typename", box "Dog" + "name", upcast "Odie" + "barks", upcast true + ] + ] + ] + + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + +[] +let ``Executes covariance for StructNullable interface field implemented by non-null object field`` () = + let hasChildType = + Define.Interface( + name = "HasChildStructNullable", + fields = [ Define.Field("child", StructNullable NamedType, fun _ (x : IHasChild) -> ValueSome x.Child) ]) + + let parentWithDogType = + Define.Object( + name = "ParentWithDogStructNullable", + isTypeOf = is, + interfaces = [ hasChildType ], + fields = [ Define.Field("child", DogType, fun _ x -> x.Child) ]) + + let queryType = + Define.Object( + name = "QueryStructNullable", + fields = [ Define.Field("parent", hasChildType, fun _ _ -> ({ Child = odie } :> IHasChild)) ]) + + let covariantSchema = + Schema(query = queryType, config = { SchemaConfig.Default with Types = [ parentWithDogType :> NamedDef ] }) + + let ast = parse """{ parent { __typename child { __typename name ... on Dog { barks } } } }""" + let result = sync <| Executor(covariantSchema).AsyncExecute(ast, getMockInputContext) + + let expected = + NameValueLookup.ofList [ + "parent", upcast NameValueLookup.ofList [ + "__typename", box "ParentWithDogStructNullable" + "child", upcast NameValueLookup.ofList [ + "__typename", box "Dog" + "name", upcast "Odie" + "barks", upcast true + ] + ] + ] + + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) From be74f88ae8ae2e14bb6a192cb5ccc30af4a379aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 01:10:44 +0200 Subject: [PATCH 21/32] Bump @protobufjs/utf8 from 1.1.0 to 1.1.1 in /samples/client-provider/file-upload/server (#558) Signed-off-by: dependabot[bot] --- .../client-provider/file-upload/server/package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/client-provider/file-upload/server/package-lock.json b/samples/client-provider/file-upload/server/package-lock.json index 7f3441f07..752bcabff 100644 --- a/samples/client-provider/file-upload/server/package-lock.json +++ b/samples/client-provider/file-upload/server/package-lock.json @@ -433,9 +433,10 @@ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" }, "node_modules/@types/accepts": { "version": "1.3.5", From c072f8e6a195b0b8ddb3c3948c580268bb611208 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:42:58 +0200 Subject: [PATCH 22/32] Bump brace-expansion from 1.1.14 to 1.1.18 in /samples/graphiql-client (#592) Signed-off-by: dependabot[bot] --- samples/graphiql-client/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/graphiql-client/package-lock.json b/samples/graphiql-client/package-lock.json index beecf1dcd..6e41e347d 100644 --- a/samples/graphiql-client/package-lock.json +++ b/samples/graphiql-client/package-lock.json @@ -2127,9 +2127,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { From 7de55d4c524e100c0d30397f21afb58cd1603884 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:27:51 +0200 Subject: [PATCH 23/32] Bump ws from 7.5.7 to 7.5.13 in /samples/client-provider/file-upload/server (#586) Signed-off-by: dependabot[bot] --- .../client-provider/file-upload/server/package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/client-provider/file-upload/server/package-lock.json b/samples/client-provider/file-upload/server/package-lock.json index 752bcabff..f6736119a 100644 --- a/samples/client-provider/file-upload/server/package-lock.json +++ b/samples/client-provider/file-upload/server/package-lock.json @@ -6956,9 +6956,10 @@ } }, "node_modules/ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "license": "MIT", "engines": { "node": ">=8.3.0" }, From 28775fd1cd0c4eee2d27d3b8cbc1a1a83554523c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:37:21 +0200 Subject: [PATCH 24/32] Bump websocket-driver from 0.7.0 to 0.7.5 in /samples/graphiql-client (#590) Signed-off-by: dependabot[bot] --- samples/graphiql-client/package-lock.json | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/samples/graphiql-client/package-lock.json b/samples/graphiql-client/package-lock.json index 6e41e347d..eacd6ec58 100644 --- a/samples/graphiql-client/package-lock.json +++ b/samples/graphiql-client/package-lock.json @@ -4375,10 +4375,11 @@ } }, "node_modules/http-parser-js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.0.tgz", - "integrity": "sha512-cZdEF7r4gfRIq7ezX9J0T+kQmJNOub71dWbgAXVHDct80TKP4MCETtZQ31xyv38UwgzkWPYF/Xc0ge55dW9Z9w==", - "dev": true + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" }, "node_modules/http-proxy": { "version": "1.17.0", @@ -7768,12 +7769,14 @@ } }, "node_modules/websocket-driver": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", - "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "http-parser-js": ">=0.4.0", + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" }, "engines": { From 47f83001c57f3d65cd264a1cd7dfbcdef5ebdcce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:45:39 +0200 Subject: [PATCH 25/32] Bump lodash from 4.17.21 to 4.18.1 in /samples/client-provider/file-upload/server (#561) Signed-off-by: dependabot[bot] --- .../client-provider/file-upload/server/package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/client-provider/file-upload/server/package-lock.json b/samples/client-provider/file-upload/server/package-lock.json index f6736119a..bff2141a1 100644 --- a/samples/client-provider/file-upload/server/package-lock.json +++ b/samples/client-provider/file-upload/server/package-lock.json @@ -4781,9 +4781,10 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" }, "node_modules/lodash.sortby": { "version": "4.7.0", From b86114cca2c6074f89c98f64540f05e699d1aed7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:58:11 +0200 Subject: [PATCH 26/32] Bump decode-uri-component from 0.2.0 to 0.2.2 in /samples/graphiql-client (#408) Signed-off-by: dependabot[bot] --- samples/graphiql-client/package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/graphiql-client/package-lock.json b/samples/graphiql-client/package-lock.json index eacd6ec58..d5c6d41c0 100644 --- a/samples/graphiql-client/package-lock.json +++ b/samples/graphiql-client/package-lock.json @@ -2639,10 +2639,11 @@ } }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10" } From ddf3b1a6836905573fb3aa6db22a136612499d14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:14:08 +0200 Subject: [PATCH 27/32] Bump nanoid and shortid in /samples/client-provider/file-upload/server (#593) Signed-off-by: dependabot[bot] --- .../file-upload/server/package-lock.json | 31 +++++++++++++------ .../file-upload/server/package.json | 2 +- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/samples/client-provider/file-upload/server/package-lock.json b/samples/client-provider/file-upload/server/package-lock.json index bff2141a1..473f0cab6 100644 --- a/samples/client-provider/file-upload/server/package-lock.json +++ b/samples/client-provider/file-upload/server/package-lock.json @@ -13,7 +13,7 @@ "lowdb": "^1.0.0", "mkdirp": "^0.5.1", "promises-all": "^1.0.0", - "shortid": "^2.2.14" + "shortid": "^2.2.17" }, "devDependencies": { "eslint": "^10.2.1", @@ -5030,9 +5030,22 @@ "optional": true }, "node_modules/nanoid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.0.1.tgz", - "integrity": "sha512-k1u2uemjIGsn25zmujKnotgniC/gxQ9sdegdezeDiKdkDW56THUMqlz3urndKCXJxA6yPzSZbXx/QCMe/pxqsA==" + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, "node_modules/nanomatch": { "version": "1.2.13", @@ -6016,12 +6029,12 @@ } }, "node_modules/shortid": { - "version": "2.2.14", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.14.tgz", - "integrity": "sha512-4UnZgr9gDdA1kaKj/38IiudfC3KHKhDc1zi/HSxd9FQDR0VLwH3/y79tZJLsVYPsJgIjeHjqIWaWVRJUj9qZOQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.17.tgz", + "integrity": "sha512-GpbM3gLF1UUXZvQw6MCyulHkWbRseNO4cyBEZresZRorwl1+SLu1ZdqgVtuwqz8mB6RpwPkm541mYSqrKyJSaA==", + "license": "MIT", "dependencies": { - "nanoid": "^2.0.0" + "nanoid": "^3.3.8" } }, "node_modules/side-channel": { diff --git a/samples/client-provider/file-upload/server/package.json b/samples/client-provider/file-upload/server/package.json index 5cb49e8e7..743811f6b 100644 --- a/samples/client-provider/file-upload/server/package.json +++ b/samples/client-provider/file-upload/server/package.json @@ -18,7 +18,7 @@ "lowdb": "^1.0.0", "mkdirp": "^0.5.1", "promises-all": "^1.0.0", - "shortid": "^2.2.14" + "shortid": "^2.2.17" }, "devDependencies": { "eslint": "^10.2.1", From 9b0c55f504468a14cbafab2eeea170d25d94f06d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:48:32 +0200 Subject: [PATCH 28/32] Bump got and nodemon in /samples/client-provider/file-upload/server (#395) Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Signed-off-by: dependabot[bot] --- .../file-upload/server/package-lock.json | 3177 ++--------------- .../file-upload/server/package.json | 4 +- 2 files changed, 265 insertions(+), 2916 deletions(-) diff --git a/samples/client-provider/file-upload/server/package-lock.json b/samples/client-provider/file-upload/server/package-lock.json index 473f0cab6..ef6ae5bcf 100644 --- a/samples/client-provider/file-upload/server/package-lock.json +++ b/samples/client-provider/file-upload/server/package-lock.json @@ -23,11 +23,11 @@ "eslint-plugin-import-order-alphabetical": "^0.0.2", "eslint-plugin-node": "^8.0.1", "eslint-plugin-prettier": "^3.0.1", - "nodemon": "^1.18.11", + "nodemon": "^3.1.14", "prettier": "^1.16.4" }, "engines": { - "node": ">=8.6" + "node": ">=10" } }, "node_modules/@apollo/protobufjs": { @@ -696,61 +696,23 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "dependencies": { - "string-width": "^2.0.0" - } - }, - "node_modules/ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" }, "node_modules/anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { - "remove-trailing-separator": "^1.0.1" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, "node_modules/apollo-cache-control": { @@ -1144,57 +1106,6 @@ "node": ">= 6.0.0" } }, - "node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/async-each": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.2.tgz", - "integrity": "sha512-6xrbvN0MOBKSJDdonmSSz2OwFSgxRaVtBDes26mj9KIGtDo+g9xosFRSC+i1gQh2oAN/tQ62AI/pGZGQjVOiRg==", - "dev": true - }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -1203,18 +1114,6 @@ "retry": "0.13.1" } }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -1226,83 +1125,17 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "deprecated": "Please upgrade to v1.0.1", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "deprecated": "Please upgrade to v1.0.1", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bluebird": { @@ -1310,24 +1143,6 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==" }, - "node_modules/boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dev": true, - "dependencies": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1339,36 +1154,16 @@ } }, "node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "is-extendable": "^0.1.0" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/busboy": { @@ -1390,26 +1185,6 @@ "node": ">= 0.8" } }, - "node_modules/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cache-content-type": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", @@ -1434,107 +1209,29 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chokidar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz", - "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==", - "dev": true, - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "optionalDependencies": { - "fsevents": "^1.2.7" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "optional": true - }, - "node_modules/ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" + "node": ">= 8.10.0" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, "node_modules/co": { @@ -1557,68 +1254,17 @@ "type-is": "^1.6.16" } }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "node_modules/component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "node_modules/configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", - "dev": true, - "dependencies": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/contains-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", @@ -1659,15 +1305,6 @@ "node": ">= 0.8" } }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/copy-to": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", @@ -1684,24 +1321,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "node_modules/create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "dependencies": { - "capture-stack-trace": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1766,15 +1385,6 @@ "node": ">= 8" } }, - "node_modules/crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/cssfilter": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", @@ -1788,29 +1398,11 @@ "ms": "2.0.0" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1833,59 +1425,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-property/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "deprecated": "Please upgrade to v1.0.1", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-property/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "deprecated": "Please upgrade to v1.0.1", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-property/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -1920,18 +1459,6 @@ "node": ">=4.5.0" } }, - "node_modules/dot-prop": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", - "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", - "dev": true, - "dependencies": { - "is-obj": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/dotenv": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", @@ -1940,12 +1467,6 @@ "node": ">=6" } }, - "node_modules/duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2030,15 +1551,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", @@ -2708,210 +2220,6 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, - "node_modules/execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "dependencies": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa/node_modules/cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "dependencies": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "node_modules/execa/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/execa/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend-shallow/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "deprecated": "Please upgrade to v1.0.1", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "deprecated": "Please upgrade to v1.0.1", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2951,30 +2259,16 @@ } }, "node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { - "is-extendable": "^0.1.0" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/find-up": { @@ -3018,27 +2312,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -3055,674 +2328,71 @@ "node": ">=8.5" } }, - "node_modules/fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dev": true, - "optional": true, - "dependencies": { - "minipass": "^2.6.0" - } - }, "node_modules/fsevents": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", - "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", - "bundleDependencies": [ - "node-pre-gyp" - ], - "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], - "dependencies": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, "engines": { - "node": ">=4.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/fsevents/node_modules/abbrev": { + "node_modules/function-bind": { "version": "1.1.1", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "inBundle": true, - "optional": true + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "node_modules/fsevents/node_modules/ansi-regex": { - "version": "2.1.1", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "inBundle": true, - "optional": true, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fsevents/node_modules/aproba": { - "version": "1.2.0", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "inBundle": true, - "optional": true + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/fsevents/node_modules/are-we-there-yet": { - "version": "1.1.5", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "deprecated": "This package is no longer supported.", - "dev": true, - "inBundle": true, - "optional": true, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fsevents/node_modules/code-point-at": { - "version": "1.1.0", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", "dev": true, - "inBundle": true, - "optional": true, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/console-control-strings": { - "version": "1.1.0", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/core-util-is": { - "version": "1.0.2", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/debug": { - "version": "2.6.9", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/fsevents/node_modules/deep-extend": { - "version": "0.6.0", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "inBundle": true, - "optional": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/fsevents/node_modules/delegates": { - "version": "1.0.0", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/detect-libc": { - "version": "1.0.3", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "inBundle": true, - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/fsevents/node_modules/fs.realpath": { - "version": "1.0.0", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/gauge": { - "version": "2.7.4", - "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/fsevents/node_modules/glob": { - "version": "7.1.3", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/fsevents/node_modules/has-unicode": { - "version": "2.0.1", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/iconv-lite": { - "version": "0.4.24", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/ignore-walk": { - "version": "3.0.1", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "minimatch": "^3.0.4" - } - }, - "node_modules/fsevents/node_modules/inflight": { - "version": "1.0.6", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/fsevents/node_modules/inherits": { - "version": "2.0.3", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/isarray": { - "version": "1.0.0", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/minimist": { - "version": "0.0.8", - "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/mkdirp": { - "version": "0.5.1", - "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", - "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "minimist": "0.0.8" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/fsevents/node_modules/ms": { - "version": "2.0.0", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/needle": { - "version": "2.2.4", - "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 0.10.x" - } - }, - "node_modules/fsevents/node_modules/node-pre-gyp": { - "version": "0.10.3", - "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", - "deprecated": "Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/fsevents/node_modules/nopt": { - "version": "4.0.1", - "integrity": "sha512-+5XZFpQZEY0cg5JaxLwGxDlKNKYxuXwGt8/Oi3UXm5/4ymrJve9d2CURituxv3rSrVCGZj4m1U1JlHTdcKt2Ng==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/fsevents/node_modules/npm-bundled": { - "version": "1.0.5", - "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/npm-packlist": { - "version": "1.2.0", - "integrity": "sha512-7Mni4Z8Xkx0/oegoqlcao/JpPCPEMtUvsmB0q7mgvlMinykJLSRTYuFqoQLYgGY8biuxIeiHO+QNJKbCfljewQ==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "node_modules/fsevents/node_modules/npmlog": { - "version": "4.1.2", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "node_modules/fsevents/node_modules/number-is-nan": { - "version": "1.0.1", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "dev": true, - "inBundle": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/object-assign": { - "version": "4.1.1", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "inBundle": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/once": { - "version": "1.4.0", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/fsevents/node_modules/os-homedir": { - "version": "1.0.2", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true, - "inBundle": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/os-tmpdir": { - "version": "1.0.2", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "inBundle": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/osenv": { - "version": "0.1.5", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/fsevents/node_modules/path-is-absolute": { - "version": "1.0.1", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "inBundle": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/process-nextick-args": { - "version": "2.0.0", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/rc": { - "version": "1.2.8", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/fsevents/node_modules/rc/node_modules/minimist": { - "version": "1.2.0", - "integrity": "sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/readable-stream": { - "version": "2.3.6", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/fsevents/node_modules/rimraf": { - "version": "2.6.3", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/fsevents/node_modules/safe-buffer": { - "version": "5.1.2", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/safer-buffer": { - "version": "2.1.2", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/sax": { - "version": "1.2.4", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/semver": { - "version": "5.6.0", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", - "dev": true, - "inBundle": true, - "optional": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/fsevents/node_modules/set-blocking": { - "version": "2.0.0", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/signal-exit": { - "version": "3.0.2", - "integrity": "sha512-meQNNykwecVxdu1RlYMKpQx4+wefIYpmxi6gexo/KAbwquJrBUrBmKYJrE8KFkVQAAVWEnwNdu21PgrD77J3xA==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/string_decoder": { - "version": "1.1.1", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/fsevents/node_modules/string-width": { - "version": "1.0.2", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/strip-ansi": { - "version": "3.0.1", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/strip-json-comments": { - "version": "2.0.1", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "inBundle": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fsevents/node_modules/util-deprecate": { - "version": "1.0.2", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/fsevents/node_modules/wide-align": { - "version": "1.1.3", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "inBundle": true, - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, - "node_modules/fsevents/node_modules/wrappy": { - "version": "1.0.2", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "inBundle": true, - "optional": true - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "engines": { - "node": ">=4" + "node": ">=4" } }, "node_modules/get-symbol-description": { @@ -3740,69 +2410,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "dependencies": { - "ini": "^1.3.4" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=4" + "node": ">= 6" } }, "node_modules/graceful-fs": { @@ -3938,8 +2556,9 @@ "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3980,45 +2599,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -4079,15 +2659,6 @@ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", "dev": true }, - "node_modules/import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4110,12 +2681,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, "node_modules/internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -4129,31 +2694,6 @@ "node": ">= 0.4" } }, - "node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "deprecated": "Please upgrade to v0.1.7", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4172,15 +2712,16 @@ } }, "node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { - "binary-extensions": "^1.0.0" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/is-boolean-object": { @@ -4198,12 +2739,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, "node_modules/is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", @@ -4215,43 +2750,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "dependencies": { - "ci-info": "^1.5.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "deprecated": "Please upgrade to v0.1.5", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -4266,38 +2764,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4307,15 +2773,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/is-generator-function": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", @@ -4337,19 +2794,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "dev": true, - "dependencies": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -4361,84 +2805,28 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "dependencies": { - "path-is-inside": "^1.0.1" - }, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=0.12.0" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "dependencies": { - "isobject": "^3.0.1" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-promise": { @@ -4446,15 +2834,6 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, - "node_modules/is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -4470,15 +2849,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -4490,15 +2860,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -4538,15 +2899,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -4558,15 +2910,6 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/iterall": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz", @@ -4616,15 +2959,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/koa": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/koa/-/koa-2.7.0.tgz", @@ -4726,18 +3060,6 @@ "any-promise": "^1.1.0" } }, - "node_modules/latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "dependencies": { - "package-json": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4823,15 +3145,6 @@ "node": ">=4" } }, - "node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4843,39 +3156,6 @@ "node": ">=10" } }, - "node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -4892,30 +3172,6 @@ "node": ">= 0.6" } }, - "node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mime-db": { "version": "1.38.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", @@ -4952,59 +3208,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, - "node_modules/minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "optional": true, - "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "node_modules/minipass/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "optional": true - }, - "node_modules/minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, - "optional": true, - "dependencies": { - "minipass": "^2.9.0" - } - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", @@ -5022,13 +3225,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, - "node_modules/nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", - "dev": true, - "optional": true - }, "node_modules/nanoid": { "version": "3.3.18", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", @@ -5047,28 +3243,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5103,28 +3277,109 @@ } }, "node_modules/nodemon": { - "version": "1.18.11", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.11.tgz", - "integrity": "sha512-KdN3tm1zkarlqNo4+W9raU3ihM4H15MVMSE/f9rYDZmFgDHAfAJsomYrHhApAkuUemYjFyEeXlpCOQ2v5gtBEw==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", "dev": true, - "hasInstallScript": true, + "license": "MIT", "dependencies": { - "chokidar": "^2.1.5", - "debug": "^3.1.0", + "chokidar": "^3.5.2", + "debug": "^4", "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.6", - "semver": "^5.5.0", - "supports-color": "^5.2.0", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.5.0" + "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/nopt": { @@ -5159,56 +3414,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5237,18 +3443,6 @@ "node": ">= 10.12.0" } }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.assign": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", @@ -5282,18 +3476,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -5325,16 +3507,7 @@ "word-wrap": "^1.2.5" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "engines": { - "node": ">=4" + "node": ">= 0.8.0" } }, "node_modules/p-limit": { @@ -5370,21 +3543,6 @@ "node": ">=6" } }, - "node_modules/package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "dependencies": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -5406,21 +3564,6 @@ "node": ">= 0.8" } }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, "node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -5430,30 +3573,6 @@ "node": ">=4" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -5480,6 +3599,19 @@ "node": ">=4" } }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -5558,15 +3690,6 @@ "node": ">=4" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5577,15 +3700,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/prettier": { "version": "1.16.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz", @@ -5610,12 +3724,6 @@ "node": ">=6.0.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, "node_modules/promises-all": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/promises-all/-/promises-all-1.0.0.tgz", @@ -5624,17 +3732,12 @@ "bluebird": "^3.4.7" } }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, "node_modules/pstree.remy": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.6.tgz", - "integrity": "sha512-NdF35+QsqD7EgNEI5mkI/X+UwaxVEbQaz9f4IooEmMUv6ZPmlTQYGjBPJGgrlzNdjSvIy4MWMg6Q6vCgBO2K+w==", - "dev": true + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", @@ -5723,27 +3826,6 @@ "node": ">=0.6" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -5771,52 +3853,17 @@ "node": ">=6" } }, - "node_modules/readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, "node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" + "picomatch": "^2.2.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.10.0" } }, "node_modules/regexp.prototype.flags": { @@ -5844,52 +3891,6 @@ "node": ">=6.5.0" } }, - "node_modules/registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", - "dev": true, - "dependencies": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "dependencies": { - "rc": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "node_modules/repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/resolve": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", @@ -5899,22 +3900,6 @@ "path-parse": "^1.0.6" } }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "dev": true - }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "engines": { - "node": ">=0.12" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -5928,15 +3913,6 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "dependencies": { - "ret": "~0.1.10" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5951,45 +3927,6 @@ "semver": "bin/semver" } }, - "node_modules/semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "dependencies": { - "semver": "^5.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", @@ -6007,27 +3944,6 @@ "sha.js": "bin.js" } }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/shortid": { "version": "2.2.17", "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.17.tgz", @@ -6050,184 +3966,32 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "deprecated": "Please upgrade to v1.0.1", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "deprecated": "Please upgrade to v1.0.1", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, + "license": "MIT", "dependencies": { - "is-extendable": "^0.1.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, - "dependencies": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", - "dev": true - }, "node_modules/spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -6260,43 +4024,6 @@ "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", "dev": true }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -6321,28 +4048,6 @@ "node": ">=0.8.0" } }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/string.prototype.trimend": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", @@ -6369,18 +4074,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6390,24 +4083,6 @@ "node": ">=4" } }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/subscriptions-transport-ws": { "version": "0.9.19", "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz", @@ -6429,6 +4104,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -6444,145 +4120,17 @@ "node": ">=0.10.0" } }, - "node_modules/tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "optional": true, - "dependencies": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "engines": { - "node": ">=4.5" - } - }, - "node_modules/tar/node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true, - "optional": true - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "optional": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/tar/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "optional": true - }, - "node_modules/tar/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "optional": true - }, - "node_modules/term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true, - "dependencies": { - "execa": "^0.7.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "is-number": "^7.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.0" } }, "node_modules/toidentifier": { @@ -6676,33 +4224,6 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "dependencies": { - "crypto-random-string": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6711,100 +4232,6 @@ "node": ">= 0.8" } }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "node_modules/unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", - "dev": true, - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "node_modules/update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "dependencies": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6820,40 +4247,6 @@ "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==" }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "dev": true - }, - "node_modules/url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, - "dependencies": { - "prepend-http": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, "node_modules/util.promisify": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.1.1.tgz", @@ -6909,18 +4302,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -6936,18 +4317,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", - "dev": true, - "dependencies": { - "string-width": "^2.1.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6958,17 +4327,6 @@ "node": ">=0.10.0" } }, - "node_modules/write-file-atomic": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", - "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, "node_modules/ws": { "version": "7.5.13", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", @@ -6990,15 +4348,6 @@ } } }, - "node_modules/xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/xss": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.11.tgz", diff --git a/samples/client-provider/file-upload/server/package.json b/samples/client-provider/file-upload/server/package.json index 743811f6b..49da380f0 100644 --- a/samples/client-provider/file-upload/server/package.json +++ b/samples/client-provider/file-upload/server/package.json @@ -8,7 +8,7 @@ "url": "https://jaydenseric.com" }, "engines": { - "node": ">=8.6" + "node": ">=10" }, "dependencies": { "apollo-server-koa": "^2.14.2", @@ -28,7 +28,7 @@ "eslint-plugin-import-order-alphabetical": "^0.0.2", "eslint-plugin-node": "^8.0.1", "eslint-plugin-prettier": "^3.0.1", - "nodemon": "^1.18.11", + "nodemon": "^3.1.14", "prettier": "^1.16.4" }, "scripts": { From 99f053d687e9166bfbbbc5762c0b1f1774f60a5d Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 12 Aug 2026 16:45:52 +0200 Subject: [PATCH 29/32] Implemented schema validation for interfaces having implementations; added tests * Added schema validation to ensure every interface has at least one implementing object type, returning a validation error if not. Introduced a `Validate()` method on the schema. * Updated `getPossibleTypes` for interfaces to use `Map.vtryFind` and `ValueOption` for safer lookups. * Added `InterfaceMissingImplementationTests.fs` with tests for orphaned interfaces and registered it in the test project file. --- src/FSharp.Data.GraphQL.Server/Schema.fs | 26 +++- .../FSharp.Data.GraphQL.Tests.fsproj | 1 + .../InterfaceMissingImplementationTests.fs | 146 ++++++++++++++++++ 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 tests/FSharp.Data.GraphQL.Tests/InterfaceMissingImplementationTests.fs diff --git a/src/FSharp.Data.GraphQL.Server/Schema.fs b/src/FSharp.Data.GraphQL.Server/Schema.fs index 67146948d..b7752c827 100644 --- a/src/FSharp.Data.GraphQL.Server/Schema.fs +++ b/src/FSharp.Data.GraphQL.Server/Schema.fs @@ -213,7 +213,7 @@ type Schema<'Root> (query: ObjectDef<'Root>, ?mutation: ObjectDef<'Root>, ?subsc let getPossibleTypes abstractDef = match abstractDef with | Union u -> u.Options - | Interface i -> Map.find i.Name (implementations.Force()) |> Array.ofList + | Interface i -> implementations.Force() |> Map.vtryFind i.Name |> ValueOption.defaultValue [] |> Array.ofList | _ -> [||] let rec introspectTypeRef isNullable (namedTypes: Map) typedef = @@ -354,6 +354,26 @@ type Schema<'Root> (query: ObjectDef<'Root>, ?mutation: ObjectDef<'Root>, ?subsc Types = itypes Directives = idirectives } + let validateSchemaAbstractions() = + let errors = ResizeArray() + + // Validate that all interfaces have at least one implementing type + typeMap.ToSeq() + |> Seq.iter (fun (_, typedef) -> + match typedef with + | Interface idef -> + let possibleTypes = getPossibleTypes typedef + if Array.isEmpty possibleTypes then + errors.Add( + GQLProblemDetails.CreateWithKind ( + $"Interface '%s{idef.Name}' has no implementing object types", + Validation + ) + ) + | _ -> ()) + + errors.ToArray() + let introspected = lazy (introspectSchema typeMap) interface ISchema with @@ -383,3 +403,7 @@ type Schema<'Root> (query: ObjectDef<'Root>, ?mutation: ObjectDef<'Root>, ?subsc interface System.Collections.IEnumerable with member _.GetEnumerator() = (typeMap.ToSeq() |> Seq.map snd :> System.Collections.IEnumerable).GetEnumerator() + + /// Validates the schema abstractions for GraphQL compliance + /// Returns array of validation errors, empty if schema is valid + member _.Validate() = validateSchemaAbstractions() 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 9615ba1a9..30ce2582a 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -41,6 +41,7 @@ + diff --git a/tests/FSharp.Data.GraphQL.Tests/InterfaceMissingImplementationTests.fs b/tests/FSharp.Data.GraphQL.Tests/InterfaceMissingImplementationTests.fs new file mode 100644 index 000000000..5573eca66 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/InterfaceMissingImplementationTests.fs @@ -0,0 +1,146 @@ +// The MIT License (MIT) +// Copyright (c) 2016 Bazinga Technologies Inc + +module FSharp.Data.GraphQL.Tests.InterfaceMissingImplementationTests + +open Xunit +open System +open System.Threading.Tasks + +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Types +open FSharp.Data.GraphQL.Parser +open FSharp.Data.GraphQL.Shared +open FSharp.Data.GraphQL.Execution +open Helpers + +#nowarn "25" + +module internal GQLProblemDetails = + let CreateSchemaValidation message = GQLProblemDetails.CreateWithKind (message, Validation) + +/// Test data types for interface validation + +type Person = { Name : string } + +type Company = { Name : string; EmployeeCount : int } + +/// Define interfaces and types for testing + +let EntityInterface = + Define.Interface (name = "Entity", fields = [ Define.Field ("name", StringType, resolve = (fun _ _ -> "")) ]) + +let PersonType = + Define.Object ( + name = "Person", + isTypeOf = (fun o -> o :? Person), + interfaces = [ EntityInterface ], + fields = [ Define.Field ("name", StringType, resolve = (fun _ person -> person.Name)) ] + ) + +let CompanyType = + Define.Object ( + name = "Company", + isTypeOf = (fun o -> o :? Company), + interfaces = [ EntityInterface ], + fields = [ + Define.Field ("name", StringType, resolve = (fun _ company -> company.Name)) + Define.Field ("employeeCount", IntType, resolve = (fun _ company -> company.EmployeeCount)) + ] + ) + +[] +let ``Schema creation with orphaned interface returns validation error`` () = + let orphanedInterface = + Define.Interface (name = "OrphanedEntity", fields = [ Define.Field ("id", StringType, resolve = (fun _ _ -> "")) ]) + + let simpleObject = + Define.Object ( + name = "SimpleObject", + isTypeOf = (fun o -> o :? Person), + interfaces = [], + fields = [ Define.Field ("name", StringType, resolve = (fun _ person -> person.Name)) ] + ) + + // According to GraphQL spec: interfaces must have at least one implementing type + let schema = + Schema (query = simpleObject, config = { SchemaConfig.Default with Types = [ orphanedInterface ] }) + + // Validate the schema - should report orphaned interface error + let validationErrors = schema.Validate () + let expected = [ + GQLProblemDetails.CreateSchemaValidation "Interface 'OrphanedEntity' has no implementing object types" + ] + equals expected (validationErrors |> Array.toList) + +[] +let ``Validation detects orphaned interface`` () = + let orphanedInterface = + Define.Interface (name = "Orphaned", fields = [ Define.Field ("field", StringType, resolve = (fun _ _ -> "")) ]) + + let simpleObject = + Define.Object ( + name = "SimpleObject", + isTypeOf = (fun o -> o :? Person), + interfaces = [], + fields = [ Define.Field ("name", StringType, resolve = (fun _ person -> person.Name)) ] + ) + + let schema = + Schema (query = simpleObject, config = { SchemaConfig.Default with Types = [ orphanedInterface ] }) + + // Validation should detect the orphaned interface + let validationErrors = schema.Validate () + nonEmpty validationErrors + +[] +let ``Interface with implementations passes validation`` () : Task = task { + let schema = Schema (query = PersonType, config = { SchemaConfig.Default with Types = [ CompanyType ] }) + + let introspectionQuery = + """ + { + __type(name: "Entity") { + name + kind + possibleTypes { + name + } + } + } + """ + + let ast = parse introspectionQuery + let! result = Executor(schema).AsyncExecute (ast, getMockInputContext) + + Assert.NotNull (result) + + let validationErrors = schema.Validate () + empty validationErrors +} + +[] +let ``Mixed schema validation detects unimplemented interface`` () = + let implementedInterface = + Define.Interface (name = "Implemented", fields = [ Define.Field ("name", StringType, resolve = (fun _ _ -> "")) ]) + + let unimplementedInterface = + Define.Interface (name = "Unimplemented", fields = [ Define.Field ("value", StringType, resolve = (fun _ _ -> "")) ]) + + let implementingType = + Define.Object ( + name = "ImplementsOne", + isTypeOf = (fun o -> o :? Person), + interfaces = [ implementedInterface ], + fields = [ Define.Field ("name", StringType, resolve = (fun _ person -> person.Name)) ] + ) + + let schema = + Schema (query = implementingType, config = { SchemaConfig.Default with Types = [ unimplementedInterface ] }) + + // Schema validation should detect unimplemented interface + let validationErrors = schema.Validate () + let expected = [ + GQLProblemDetails.CreateSchemaValidation "Interface 'Unimplemented' has no implementing object types" + ] + equals expected (validationErrors |> Array.toList) From 8ed90b2c6197ec3c889ccba1badf9e7d106dd353 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 12 Aug 2026 16:47:04 +0200 Subject: [PATCH 30/32] fix(parser): allow zero whitespace between `}` and next definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Named operations used `token_ws selectionSet`, which applies `notFollowedBy (letter|digit|_)` after the selection set. That rejected valid minified documents where a punctuator `}` is immediately followed by a Name (`fragment`, `query`, …), e.g. graphql-js `getIntrospectionQuery()` output and tooling such as GraphQL Inspector. Selection sets end with the punctuator `}`; consume trailing Ignored with `selectionSet .>> whitespaces` instead of `token_ws`. Per GraphQL spec (Language / Source Text): - Lexical tokens may be separated by Ignored tokens (Whitespace, etc.) - Any amount of Ignored may appear before/after every token (including zero) - Whitespace is required only when consecutive SourceCharacters would otherwise form a single token (maximal munch); `}` cannot start a Name, so `}fragment` and `}query` are two tokens without a separator Spec: https://spec.graphql.org/draft/#sec-Source-Text.Ignored-Tokens https://spec.graphql.org/October2021/#sec-Source-Text.Ignored-Tokens Tests: minified shorthand/named query + fragment, adjacent named queries, and a multi-fragment introspection-shaped document. --- src/FSharp.Data.GraphQL.Shared/Parser.fs | 3 +- .../FSharp.Data.GraphQL.Tests/ParserTests.fs | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Shared/Parser.fs b/src/FSharp.Data.GraphQL.Shared/Parser.fs index f035f9a33..d049a4192 100644 --- a/src/FSharp.Data.GraphQL.Shared/Parser.fs +++ b/src/FSharp.Data.GraphQL.Shared/Parser.fs @@ -316,7 +316,8 @@ module internal Internal = (opt (token_ws name)) (opt (token_ws variableDefinitions)) (opt (token_ws directives)) - (token_ws selectionSet) + // '}' is a punctuator; token_ws would reject `}fragment` / `}query` (no whitespace). + (selectionSet .>> whitespaces) (fun otype name ovars directives selection -> { OperationType = otype Name = name |> ValueOption.ofOption diff --git a/tests/FSharp.Data.GraphQL.Tests/ParserTests.fs b/tests/FSharp.Data.GraphQL.Tests/ParserTests.fs index efb81e2a6..257f1f62e 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ParserTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ParserTests.fs @@ -653,3 +653,38 @@ fragment frag on Friend { [] let ``Parser must parse kitchen sink``() = parse KitchenSink + +[] +let ``Parser must parse minified shorthand query followed by fragment without whitespace`` () = + let expected = + docN [ + queryWithSelection (field "__typename") + fragmentWithCondAndSelection "X" "Query" (field "__typename") + ] + test expected "{__typename}fragment X on Query{__typename}" + +[] +let ``Parser must parse minified named query followed by fragment without whitespace`` () = + let expected = + docN [ + namedQuerWithSelection "A" (field "__typename") + fragmentWithCondAndSelection "X" "Query" (field "__typename") + ] + test expected "query A{__typename}fragment X on Query{__typename}" + +[] +let ``Parser must parse minified adjacent named queries without whitespace`` () = + let expected = + docN [ + namedQuerWithSelection "A" (field "__typename") + namedQuerWithSelection "B" (field "__typename") + ] + test expected "query A{__typename}query B{__typename}" + +[] +let ``Parser must parse minified named query followed by multiple fragments without whitespace`` () = + // Shape used by graphql-js getIntrospectionQuery() and JS tooling (minified). + let query = + "query IntrospectionQuery{__schema{queryType{name}}}fragment FullType on __Type{kind name}fragment InputValue on __InputValue{name}" + let doc = parse query + equals 3 doc.Definitions.Length From 7e51ea7b41d7004be51d5feacd030b6496a97465 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 13 Aug 2026 03:33:49 +0200 Subject: [PATCH 31/32] Make `__type` introspection field nullable per spec * Updated `TypeMetaFieldDef` to return nullable `__Type` (StructNullable). * Changed resolver to use `Seq.vtryFind`, returning null for unknown types. * Added test to verify `__type` returns null for unknown type names. --- src/FSharp.Data.GraphQL.Server/Planning.fs | 6 +++--- .../FSharp.Data.GraphQL.Tests/IntrospectionTests.fs | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server/Planning.fs b/src/FSharp.Data.GraphQL.Server/Planning.fs index 673155d34..002a05989 100644 --- a/src/FSharp.Data.GraphQL.Server/Planning.fs +++ b/src/FSharp.Data.GraphQL.Server/Planning.fs @@ -27,7 +27,7 @@ let TypeMetaFieldDef = Define.Field( name = "__type", description = "Request the type information of a single type.", - typedef = __Type, + typedef = StructNullable __Type, args = [ { Name = "name" Description = None @@ -38,8 +38,8 @@ let TypeMetaFieldDef = ], resolve = fun ctx (_:obj) -> ctx.Schema.Introspected.Types - |> Seq.find (fun t -> t.Name = ctx.Arg("name")) - |> IntrospectionTypeRef.Named) + |> Seq.vtryFind (fun t -> t.Name = ctx.Arg("name")) + |> ValueOption.map IntrospectionTypeRef.Named) /// Field definition allowing to resolve a name of the current Object type at runtime. let TypeNameMetaFieldDef : FieldDef = diff --git a/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs b/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs index b04010b9e..54a731e94 100644 --- a/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs @@ -328,6 +328,19 @@ let ``Core type definitions are considered nullable`` () = empty errors data |> equals (upcast expected) +[] +let ``__type must return null for unknown type name`` () = + // Spec: `__type(name: String!): __Type` (nullable), so unknown type names must resolve to null. + // https://spec.graphql.org/draft/#sec-Schema-Introspection.Schema + let root = Define.Object("Query", [ Define.Field("onlyField", StringType) ]) + let schema = Schema(root) + let query = """{ __type(name: "DefinitelyMissingType") { name kind } }""" + let result = sync <| Executor(schema).AsyncExecute(query, getMockInputContext) + let expected = NameValueLookup.ofList [ "__type", null ] + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + type User = { FirstName: string; LastName: string } type UserInput = { Name: string } From 7ed1b615e2c30e9291a08be683e695af92cfcee4 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 22:44:24 +0200 Subject: [PATCH 32/32] Added prompts to prepare GitHub PR and issue descriptions Added a pull request template and a `create-pr-description` prompt that fills it in from the branch diff, and a `create-issue-description` prompt that fills in `ISSUE_TEMPLATE.md`. Both compare against `origin/dev`, the default branch of this repository. Co-Authored-By: Claude Opus 5 (1M context) --- .github/PULL_REQUEST_TEMPLATE.md | 24 +++++++++ .../create-issue-description.prompt.md | 24 +++++++++ .../prompts/create-pr-description.prompt.md | 54 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/prompts/create-issue-description.prompt.md create mode 100644 .github/prompts/create-pr-description.prompt.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..1f4de828e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ +## Proposed Changes + +Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. + +## Types of changes + +What types of changes does your code introduce to FSharp.Data.GraphQL? +_Put an `x` in the boxes that apply_ + +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +## Checklist + +_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ + +- [ ] Build and tests pass locally +- [ ] I have added tests that prove my fix is effective or that my feature works (if appropriate) +- [ ] I have added necessary documentation (if appropriate) + +## Further comments + +If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/.github/prompts/create-issue-description.prompt.md b/.github/prompts/create-issue-description.prompt.md new file mode 100644 index 000000000..b6f62097b --- /dev/null +++ b/.github/prompts/create-issue-description.prompt.md @@ -0,0 +1,24 @@ +--- +mode: agent +description: Generates a GitHub issue description for FSharp.Data.GraphQL by analyzing the current branch's changes and filling in the project's issue template. +--- + +Generate a GitHub issue description for the current branch by filling out the repository's issue template. + +## Steps + +1. Read #file:'.github/ISSUE_TEMPLATE.md' to understand the required sections and their purpose. +2. Run `git diff origin/dev...HEAD --stat` and `git log origin/dev...HEAD --oneline` to understand what was changed in the current branch. +3. For each section in the template, infer the appropriate content from the branch changes, commit messages, and modified file names. +4. Do **not** leave any template placeholder text (e.g., `Step A`, `Step B`) in the output – replace every section with concrete, specific content derived from the branch. +5. Write in clear, concise English suitable for a public GitHub issue. + +## Output + +A single fenced markdown code block containing the fully filled-out issue body, ready to copy and paste directly into GitHub. Do not include any explanation or commentary outside the code block. + +## Notes + +- If a template section is not applicable given the changes, write "N/A" rather than omitting the section. +- Base all content strictly on actual branch changes – do not speculate or invent scenarios. +- Commit messages, changed file paths, and any added or modified test cases are strong signals for identifying the change's purpose and scope. diff --git a/.github/prompts/create-pr-description.prompt.md b/.github/prompts/create-pr-description.prompt.md new file mode 100644 index 000000000..97c8d48bc --- /dev/null +++ b/.github/prompts/create-pr-description.prompt.md @@ -0,0 +1,54 @@ +--- +mode: agent +description: Generates a pull request description for FSharp.Data.GraphQL by analyzing the current branch's changes and filling in the project's PR template. +--- + +# Create PR Description + +## Context + +#file:'.github/PULL_REQUEST_TEMPLATE.md' + +> If the `#file:` reference above cannot be resolved, run `Get-Content .github/PULL_REQUEST_TEMPLATE.md` in PowerShell from the repository root to read the template. + +## Task + +Analyze the current branch's git diff and commit history relative to the remote base branch (`origin/dev`), then produce a fully filled-in PR description that conforms to the project's `PULL_REQUEST_TEMPLATE.md`. + +## Steps + +1. Read `#file:'.github/PULL_REQUEST_TEMPLATE.md'` to identify the exact section structure, checkbox labels, and required ordering. +2. Run `git diff origin/dev...HEAD --stat`, `git diff origin/dev...HEAD`, and `git log origin/dev...HEAD --oneline` to determine exactly what changed in the current branch. +3. Fill `## Proposed Changes` with a concise 3–6 sentence summary of the concrete behavior, API, test, or documentation changes visible in the diff. +4. Set `## Types of changes` checkboxes strictly from evidence in commits and diff: + - mark bugfix only when existing behavior is corrected + - mark new feature only when new user-facing capability is introduced + - mark breaking change only when existing public behavior or API compatibility is intentionally broken +5. Evaluate `## Checklist` strictly from branch evidence: + - mark test-related items only if tests were added or updated in the diff + - mark documentation-related items only if docs or XML comments were changed + - if build/test execution is not visible from evidence, leave relevant items unchecked and add a brief inline note +6. Replace all placeholder/template text with branch-specific content and include `## Further comments` only when the change is large or architecturally significant. + +## Guidelines + +- Be factual and precise – base every statement on the actual diff and commits, not assumptions. +- Keep the **Proposed Changes** section concise but informative (3–6 sentences max). +- For checklist items that cannot be determined from the diff alone, leave them unchecked and add a short inline note. +- Do not invent issue numbers or links unless they appear in commit messages or branch names. +- Use plain English; avoid vague filler phrases like "various improvements". +- Do not start the summary with "This branch adds". +- Use only en dashes (`–`) for dashes; never use em dashes (`—`). +- Always wrap names and versions into backticks (for example, `ObjectListFilter`) when referring to them in the description. +- Preserve all original template headings, checkbox syntax, and section order exactly. + +## Output + +A single fenced markdown code block containing the fully filled-out PR description, ready to copy and paste directly into GitHub. Do not escape any markdown syntax inside the block. Match the structure of `PULL_REQUEST_TEMPLATE.md` exactly: + +- `## Proposed Changes` – narrative paragraph(s) +- `## Types of changes` – checkboxes with `x` placed in the correct box(es) +- `## Checklist` – checkboxes filled based on evidence from the diff +- `## Further comments` – include only when the change warrants extra explanation; omit the section entirely otherwise + +Do not include any explanation or commentary outside the code block.