From 45a7d480baf628568a053a48b8eb5f86fbe53653 Mon Sep 17 00:00:00 2001 From: valber Date: Fri, 3 Mar 2023 22:37:41 +0100 Subject: [PATCH 001/100] ignoring the whole .vscode/ folder --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2afa2e272..3ab2b21a2 100644 --- a/.gitignore +++ b/.gitignore @@ -448,7 +448,4 @@ $RECYCLE.BIN/ ## Visual Studio Code ## .vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json +*/**/.vscode/* From c3f1a48c262f93e36e360a5bd4591f51303c313f Mon Sep 17 00:00:00 2001 From: valber Date: Sat, 4 Mar 2023 10:33:10 +0100 Subject: [PATCH 002/100] introducing FSharp.Data.GraphQL.Server.AppInfrastructure These changes introduce utility code for configuring usage of FSharp.Data.GraphQL in an ASP.NET app, especially one using Giraffe, AND offers an ASP.NET middleware that implements the graphql-transport-ws Websocket subprotocol for GraphQL subscriptions (a.k.a Apollo GraphQL subscription protocol). A companion sample chat application (the backend part) is also introduced with these changes. This development departed from the "start-wars-api" sample. - Originally developed in https://github.com/valbers/graphql-transport-ws.fsharp - See also: https://github.com/fsprojects/FSharp.Data.GraphQL/discussions/428 --- FSharp.Data.GraphQL.sln | 15 + README.md | 4 + build.fsx | 5 + .../client/TestData/schema-snapshot.json | 1 + .../client/TestData/subscription-example.json | 4 + samples/chat-app/server/DomainModel.fs | 106 +++ samples/chat-app/server/Program.fs | 52 ++ .../server/Properties/launchSettings.json | 37 + samples/chat-app/server/Schema.fs | 759 ++++++++++++++++++ .../server/appsettings.Development.json | 8 + samples/chat-app/server/appsettings.json | 9 + samples/chat-app/server/chat-app.fsproj | 18 + ...rp.Data.GraphQL.Samples.StarWarsApi.fsproj | 6 +- samples/star-wars-api/Helpers.fs | 45 -- samples/star-wars-api/HttpHandlers.fs | 115 --- samples/star-wars-api/JsonConverters.fs | 175 ---- samples/star-wars-api/Startup.fs | 28 +- samples/star-wars-api/WebSocketMessages.fs | 22 - samples/star-wars-api/WebSocketMiddleware.fs | 157 ---- .../Exceptions.fs | 4 + ...ta.GraphQL.Server.AppInfrastructure.fsproj | 36 + .../Giraffe/HttpHandlers.fs | 163 ++++ .../GraphQLOptions.fs | 21 + .../GraphQLSubscriptionsManagement.fs | 32 + .../GraphQLWebsocketMiddleware.fs | 376 +++++++++ .../Messages.fs | 62 ++ .../README.md | 100 +++ .../Rop/Rop.fs | 139 ++++ .../Rop/RopAsync.fs | 20 + .../Serialization/GraphQLQueryDecoding.fs | 76 ++ .../Serialization/JsonConverters.fs | 161 ++++ .../StartupExtensions.fs | 40 + .../AppInfrastructure/InvalidMessageTests.fs | 147 ++++ .../AppInfrastructure/SerializationTests.fs | 157 ++++ .../AppInfrastructure/TestSchema.fs | 229 ++++++ .../FSharp.Data.GraphQL.Tests.fsproj | 4 + 36 files changed, 2808 insertions(+), 525 deletions(-) create mode 100644 samples/chat-app/client/TestData/schema-snapshot.json create mode 100644 samples/chat-app/client/TestData/subscription-example.json create mode 100644 samples/chat-app/server/DomainModel.fs create mode 100644 samples/chat-app/server/Program.fs create mode 100644 samples/chat-app/server/Properties/launchSettings.json create mode 100644 samples/chat-app/server/Schema.fs create mode 100644 samples/chat-app/server/appsettings.Development.json create mode 100644 samples/chat-app/server/appsettings.json create mode 100644 samples/chat-app/server/chat-app.fsproj delete mode 100644 samples/star-wars-api/Helpers.fs delete mode 100644 samples/star-wars-api/HttpHandlers.fs delete mode 100644 samples/star-wars-api/JsonConverters.fs delete mode 100644 samples/star-wars-api/WebSocketMessages.fs delete mode 100644 samples/star-wars-api/WebSocketMiddleware.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/Exceptions.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLOptions.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLSubscriptionsManagement.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/Messages.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/README.md create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/Rop.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/RopAsync.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/GraphQLQueryDecoding.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/JsonConverters.fs create mode 100644 src/FSharp.Data.GraphQL.Server.AppInfrastructure/StartupExtensions.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/SerializationTests.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/TestSchema.fs diff --git a/FSharp.Data.GraphQL.sln b/FSharp.Data.GraphQL.sln index 6dbf20471..f0d1ea728 100644 --- a/FSharp.Data.GraphQL.sln +++ b/FSharp.Data.GraphQL.sln @@ -171,6 +171,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "components", "components", user.jsx = user.jsx EndProjectSection EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.GraphQL.Server.AppInfrastructure", "src\FSharp.Data.GraphQL.Server.AppInfrastructure\FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj", "{554A6833-1E72-41B4-AAC1-C19371EC061B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -289,6 +291,18 @@ Global {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Release|x64.Build.0 = Release|Any CPU {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Release|x86.ActiveCfg = Release|Any CPU {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Release|x86.Build.0 = Release|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Debug|x64.ActiveCfg = Debug|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Debug|x64.Build.0 = Debug|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Debug|x86.ActiveCfg = Debug|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Debug|x86.Build.0 = Debug|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Release|Any CPU.Build.0 = Release|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Release|x64.ActiveCfg = Release|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Release|x64.Build.0 = Release|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Release|x86.ActiveCfg = Release|Any CPU + {554A6833-1E72-41B4-AAC1-C19371EC061B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -317,6 +331,7 @@ Global {6EEA0E79-693F-4D4F-B55B-DB0C64EBDA45} = {600D4BE2-FCE0-4684-AC6F-2DC829B395BA} {7AA3516E-60F5-4969-878F-4E3DCF3E63A3} = {A8F031E0-2BD5-4BAE-830A-60CBA76A047D} {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78} + {554A6833-1E72-41B4-AAC1-C19371EC061B} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C5B9895C-9DF8-4557-8D44-7D0C4C31F86E} diff --git a/README.md b/README.md index 5b511c204..bf1caf2b2 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ let reply = executor.AsyncExecute(Parser.parse "{ firstName, lastName }", johnSn It's type safe. Things like invalid fields or invalid return types will be checked at compile time. +### ASP.NET / Giraffe / Websocket (for GraphQL subscriptions) usage + +→ See the [AppInfrastructure/README.md](src/FSharp.Data.GraphQL.Server.AppInfrastructure/README.md) + ## Demos ### GraphiQL client diff --git a/build.fsx b/build.fsx index be3802c69..dc08e5a2d 100644 --- a/build.fsx +++ b/build.fsx @@ -249,6 +249,8 @@ Target.create "PublishMiddleware" <| fun _ -> publishPackage "Server.Middleware" Target.create "PublishShared" <| fun _ -> publishPackage "Shared" +Target.create "PublishAppInfrastructure" <| fun _ -> publishPackage "Server.AppInfrastructure" + Target.create "PackServer" <| fun _ -> pack "Server" Target.create "PackClient" <| fun _ -> pack "Client" @@ -257,6 +259,8 @@ Target.create "PackMiddleware" <| fun _ -> pack "Server.Middleware" Target.create "PackShared" <| fun _ -> pack "Shared" +Target.create "PackAppInfrastructure" <| fun _ -> pack "Server.AppInfrastructure" + // -------------------------------------------------------------------------------------- // Run all targets by default. Invoke 'build -t ' to override @@ -282,6 +286,7 @@ Target.create "PackAll" ignore ==> "PackServer" ==> "PackClient" ==> "PackMiddleware" + ==> "PackAppInfrastructure" ==> "PackAll" Target.runOrDefaultWithArguments "All" diff --git a/samples/chat-app/client/TestData/schema-snapshot.json b/samples/chat-app/client/TestData/schema-snapshot.json new file mode 100644 index 000000000..4e086b01e --- /dev/null +++ b/samples/chat-app/client/TestData/schema-snapshot.json @@ -0,0 +1 @@ +{"data":{"__schema":{"queryType":{"name":"Query"},"mutationType":{"name":"Mutation"},"subscriptionType":{"name":"Subscription"},"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 String 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 ID 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":"Date","description":"The \u0060Date\u0060 scalar type represents a Date value with Time component. The Date 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":"URI","description":"The \u0060URI\u0060 scalar type represents a string resource identifier compatible with URI standard. The URI 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":null,"fields":[{"name":"organizations","description":"gets all available organizations","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Organization","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Organization","description":"An organization as seen from the outside","fields":[{"name":"chatRooms","description":"chat rooms in this organization","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatRoom","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"id","description":"the organization\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"members","description":"members of this organization","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Member","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the organization\u0027s name","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":"ChatRoom","description":"A chat room as viewed from the outside","fields":[{"name":"id","description":"the chat room\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"members","description":"the members in the chat room","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatMember","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the chat room\u0027s name","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":"SCALAR","name":"Guid","description":"The \u0060Guid\u0060 scalar type represents a Globaly 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":"OBJECT","name":"ChatMember","description":"A chat member is an organization member participating in a chat room","fields":[{"name":"id","description":"the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the member\u0027s name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":"the member\u0027s role in the chat","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"MemberRoleInChat","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"MemberRoleInChat","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ChatAdmin","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ChatGuest","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"Member","description":"An organization member","fields":[{"name":"id","description":"the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the member\u0027s name","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":"Subscription","description":null,"fields":[{"name":"chatRoomEvents","description":"events related to a specific chat room","args":[{"name":"chatRoomId","description":"the ID of the chat room to listen to events from","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatRoomEvent","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"ChatRoomEvent","description":"Something that happened in the chat room, like a new message sent","fields":[{"name":"chatRoomId","description":"the ID of the chat room in which the event happened","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"specificData","description":"the event\u0027s specific data","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"UNION","name":"ChatRoomSpecificEvent","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"time","description":"the time the message was received at the server","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Date","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"ChatRoomSpecificEvent","description":"data which is specific to a certain type of event","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"NewMessage","ofType":null},{"kind":"OBJECT","name":"EditedMessage","ofType":null},{"kind":"OBJECT","name":"DeletedMessage","ofType":null},{"kind":"OBJECT","name":"MemberJoined","ofType":null},{"kind":"OBJECT","name":"MemberLeft","ofType":null}]},{"kind":"OBJECT","name":"NewMessage","description":"a new public message has been sent in the chat room","fields":[{"name":"authorId","description":"the member ID of the message\u0027s author","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"chatRoomId","description":"the ID of the chat room the message belongs to","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"date","description":"the time the message was received at the server","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Date","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"id","description":"the message\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"text","description":"the message\u0027s text","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":"EditedMessage","description":"a public message of the chat room has been edited","fields":[{"name":"authorId","description":"the member ID of the message\u0027s author","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"chatRoomId","description":"the ID of the chat room the message belongs to","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"date","description":"the time the message was received at the server","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Date","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"id","description":"the message\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"text","description":"the message\u0027s text","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":"DeletedMessage","description":"a public message of the chat room has been deleted","fields":[{"name":"messageId","description":"this is the message ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"MemberJoined","description":"a member has joined the chat","fields":[{"name":"memberId","description":"this is the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"memberName","description":"this is the member\u0027s name","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":"MemberLeft","description":"a member has left the chat","fields":[{"name":"memberId","description":"this is the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"memberName","description":"this is the member\u0027s name","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":"Mutation","description":null,"fields":[{"name":"createChatRoom","description":"creates a new chat room for a user","args":[{"name":"organizationId","description":"the ID of the organization in which the chat room will be created","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"name","description":"the chat room\u0027s name","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatRoomForMember","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deleteChatMessage","description":null,"args":[{"name":"organizationId","description":"the ID of the organization the chat room and member are in","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"chatRoomId","description":"the chat room\u0027s ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"messageId","description":"the existing message\u0027s ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"editChatMessage","description":null,"args":[{"name":"organizationId","description":"the ID of the organization the chat room and member are in","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"chatRoomId","description":"the chat room\u0027s ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"messageId","description":"the existing message\u0027s ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"text","description":"the chat message\u0027s contents","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"enterChatRoom","description":"makes a member enter a chat room","args":[{"name":"organizationId","description":"the ID of the organization the chat room and member are in","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"chatRoomId","description":"the ID of the chat room","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatRoomForMember","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"enterOrganization","description":"makes a new member enter an organization","args":[{"name":"organizationId","description":"the ID of the organization","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"member","description":"the new member\u0027s name","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"OrganizationForMember","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"leaveChatRoom","description":"makes a member leave a chat room","args":[{"name":"organizationId","description":"the ID of the organization the chat room and member are in","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"chatRoomId","description":"the ID of the chat room","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"sendChatMessage","description":null,"args":[{"name":"organizationId","description":"the ID of the organization the chat room and member are in","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"chatRoomId","description":"the chat room\u0027s ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"text","description":"the chat message\u0027s contents","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"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":"ChatRoomForMember","description":"A chat room as viewed by a chat room member","fields":[{"name":"id","description":"the chat room\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"meAsAChatMember","description":"the chat member that queried the details","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"MeAsAChatMember","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the chat room\u0027s name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"otherChatMembers","description":"the chat members excluding the one who queried the details","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatMember","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"MeAsAChatMember","description":"A chat member is an organization member participating in a chat room","fields":[{"name":"id","description":"the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the member\u0027s name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"privId","description":"the member\u0027s private ID used for authenticating their requests","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":"the member\u0027s role in the chat","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"MemberRoleInChat","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"OrganizationForMember","description":"An organization as seen by one of the organization\u0027s members","fields":[{"name":"chatRooms","description":"chat rooms in this organization","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatRoom","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"id","description":"the organization\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"meAsAMember","description":"the member that queried the details","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"MeAsAMember","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the organization\u0027s name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"otherMembers","description":"members of this organization","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Member","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"MeAsAMember","description":"An organization member","fields":[{"name":"id","description":"the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the member\u0027s name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"privId","description":"the member\u0027s private ID used for authenticating their requests","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"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":[]}]}},"documentId":-412077120,"errors":[]} \ No newline at end of file diff --git a/samples/chat-app/client/TestData/subscription-example.json b/samples/chat-app/client/TestData/subscription-example.json new file mode 100644 index 000000000..b2ca0ce9b --- /dev/null +++ b/samples/chat-app/client/TestData/subscription-example.json @@ -0,0 +1,4 @@ +{ "id": "1", + "type": "subscribe", + "query": "subscription { chatRoomEvents (chatRoomId: \"123\", memberId: \"345\") { chatRoomId time specificData { __typename ... on MemberJoined { memberId memberName } ... on MemberLeft { memberId memberName } ... on NewMessage { id chatRoomId date authorId text } ... on EditedMessage { id chatRoomId date authorId text } ... on DeletedMessage { messageId } } } }" +} \ No newline at end of file diff --git a/samples/chat-app/server/DomainModel.fs b/samples/chat-app/server/DomainModel.fs new file mode 100644 index 000000000..5aa02e6a3 --- /dev/null +++ b/samples/chat-app/server/DomainModel.fs @@ -0,0 +1,106 @@ +namespace chat_app + +open System + +// +// Common model +// +type OrganizationId = OrganizationId of Guid +type MemberId = MemberId of Guid +type MemberPrivateId = MemberPrivateId of Guid +type ChatRoomId = ChatRoomId of Guid +type MessageId = MessageId of Guid + +type MemberRoleInChat = + | ChatAdmin + | ChatGuest + +type ChatRoomMessage = + { Id : MessageId + ChatRoomId : ChatRoomId + Date : DateTime + AuthorId : MemberId + Text : string } + +type ChatRoomSpecificEvent = + | NewMessage of ChatRoomMessage + | EditedMessage of ChatRoomMessage + | DeletedMessage of MessageId + | MemberJoined of MemberId * string + | MemberLeft of MemberId * string + +type ChatRoomEvent = + { ChatRoomId : ChatRoomId + Time : DateTime + SpecificData : ChatRoomSpecificEvent } + +// +// Persistence model +// +type Member_In_Db = + { PrivId : MemberPrivateId + Id : MemberId + Name : string } + +type ChatMember_In_Db = + { ChatRoomId : ChatRoomId + MemberId : MemberId + Role : MemberRoleInChat } + +type ChatRoom_In_Db = + { Id : ChatRoomId + Name : string + Members : MemberId list } + +type Organization_In_Db = + { Id : OrganizationId + Name : string + Members : MemberId list + ChatRooms : ChatRoomId list } + +// +// GraphQL models +// +type Member = + { Id : MemberId + Name : string } + +type MeAsAMember = + { PrivId : MemberPrivateId + Id : MemberId + Name : string } + +type ChatMember = + { Id : MemberId + Name : string + Role : MemberRoleInChat } + +type MeAsAChatMember = + { PrivId : MemberPrivateId + Id : MemberId + Name : string + Role : MemberRoleInChat } + +type ChatRoom = + { Id : ChatRoomId + Name : string + Members: ChatMember list } + +type ChatRoomForMember = + { Id : ChatRoomId + Name : string + MeAsAChatMember : MeAsAChatMember + OtherChatMembers: ChatMember list } + +type Organization = + { Id : OrganizationId + Name : string + Members : Member list + ChatRooms : ChatRoom list } + +type OrganizationForMember = + { Id : OrganizationId + Name : string + MeAsAMember : MeAsAMember + OtherMembers : Member list + ChatRooms : ChatRoom list } diff --git a/samples/chat-app/server/Program.fs b/samples/chat-app/server/Program.fs new file mode 100644 index 000000000..86b9c8d3f --- /dev/null +++ b/samples/chat-app/server/Program.fs @@ -0,0 +1,52 @@ +namespace chat_app + +open Giraffe +open FSharp.Data.GraphQL.Server.AppInfrastructure +open FSharp.Data.GraphQL.Server.AppInfrastructure.Giraffe +open System +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Server.Kestrel.Core +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Hosting +open Microsoft.Extensions.Logging + +module Program = + let rootFactory () : Root = + { RequestId = Guid.NewGuid().ToString() } + + let errorHandler (ex : Exception) (log : ILogger) = + log.LogError(EventId(), ex, "An unhandled exception has occurred while executing this request.") + clearResponse >=> setStatusCode 500 + + [] + let main args = + let builder = WebApplication.CreateBuilder(args) + builder.Services + .AddGiraffe() + .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) + .AddGraphQLOptions( + Schema.executor, + rootFactory, + "/ws" + ) + |> ignore + + let app = builder.Build() + + let applicationLifetime = app.Services.GetRequiredService() + let loggerFactory = app.Services.GetRequiredService() + + app + .UseGiraffeErrorHandler(errorHandler) + .UseWebSockets() + .UseWebSocketsForGraphQL() + .UseGiraffe + (HttpHandlers.handleGraphQL + applicationLifetime.ApplicationStopping + (loggerFactory.CreateLogger("HttpHandlers.handlerGraphQL")) + ) + + app.Run() + + 0 // Exit code + diff --git a/samples/chat-app/server/Properties/launchSettings.json b/samples/chat-app/server/Properties/launchSettings.json new file mode 100644 index 000000000..40fef14da --- /dev/null +++ b/samples/chat-app/server/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:7082", + "sslPort": 44311 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5092", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7122;http://localhost:5092", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/chat-app/server/Schema.fs b/samples/chat-app/server/Schema.fs new file mode 100644 index 000000000..b2a17baf8 --- /dev/null +++ b/samples/chat-app/server/Schema.fs @@ -0,0 +1,759 @@ +namespace chat_app + +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Types +open System + +type Root = + { RequestId : string } + +type FakePersistence() = + static let mutable _members = Map.empty + static let mutable _chatMembers = Map.empty + static let mutable _chatRoomMessages = Map.empty + static let mutable _chatRooms = Map.empty + static let mutable _organizations = + let newId = OrganizationId (Guid.Parse("51f823ef-2294-41dc-9f39-a4b9a237317a")) + ( newId, + { Organization_In_Db.Id = newId + Name = "Public" + Members = [] + ChatRooms = [] } + ) + |> List.singleton + |> Map.ofList + + static member Members + with get() = _members + and set(v) = _members <- v + + static member ChatMembers + with get() = _chatMembers + and set(v) = _chatMembers <- v + + static member ChatRoomMessages + with get() = _chatRoomMessages + and set(v) = _chatRoomMessages <- v + + static member ChatRooms + with get() = _chatRooms + and set(v) = _chatRooms <- v + + static member Organizations + with get() = _organizations + and set(v) = _organizations <- v + +module MapFrom = + let memberInDb_To_Member (x : Member_In_Db) : Member = + { Id = x.Id + Name = x.Name } + + let memberInDb_To_MeAsAMember (x : Member_In_Db) : MeAsAMember = + { PrivId = x.PrivId + Id = x.Id + Name = x.Name } + + let chatMemberInDb_To_ChatMember (membersToGetDetailsFrom : Member_In_Db seq) (x : ChatMember_In_Db) : ChatMember = + let memberDetails = membersToGetDetailsFrom |> Seq.find (fun m -> m.Id = x.MemberId) + { Id = x.MemberId + Name = memberDetails.Name + Role = x.Role } + + let chatMemberInDb_To_MeAsAChatMember (membersToGetDetailsFrom : Member_In_Db seq) (x : ChatMember_In_Db) : MeAsAChatMember = + let memberDetails = membersToGetDetailsFrom |> Seq.find (fun m -> m.Id = x.MemberId) + { PrivId = memberDetails.PrivId + Id = x.MemberId + Name = memberDetails.Name + Role = x.Role } + + let chatRoomInDb_To_ChatRoom (membersToGetDetailsFrom : Member_In_Db seq) (x : ChatRoom_In_Db) : ChatRoom = + { Id = x.Id + Name = x.Name + Members = + FakePersistence.ChatMembers.Values + |> Seq.filter (fun m -> x.Members |> List.contains m.MemberId) + |> Seq.map (chatMemberInDb_To_ChatMember membersToGetDetailsFrom) + |> List.ofSeq } + + let chatRoomInDb_To_ChatRoomForMember (membersToGetDetailsFrom : Member_In_Db seq) (chatMember : ChatMember_In_Db) (x : ChatRoom_In_Db) : ChatRoomForMember = + { Id = x.Id + Name = x.Name + MeAsAChatMember = chatMember |> chatMemberInDb_To_MeAsAChatMember membersToGetDetailsFrom + OtherChatMembers = + FakePersistence.ChatMembers.Values + |> Seq.filter (fun m -> m.MemberId <> chatMember.MemberId && x.Members |> List.contains m.MemberId) + |> Seq.map (chatMemberInDb_To_ChatMember membersToGetDetailsFrom) + |> List.ofSeq } + + let organizationInDb_To_Organization (x : Organization_In_Db) : Organization = + let members = + FakePersistence.Members.Values + |> Seq.filter (fun m -> x.Members |> List.contains m.Id) + { Id = x.Id + Name = x.Name + Members = members |> Seq.map memberInDb_To_Member |> List.ofSeq + ChatRooms = + FakePersistence.ChatRooms.Values + |> Seq.filter (fun c -> x.ChatRooms |> List.contains c.Id) + |> Seq.map (chatRoomInDb_To_ChatRoom members) + |> List.ofSeq } + + let organizationInDb_To_OrganizationForMember (memberId : MemberId) (x : Organization_In_Db) : OrganizationForMember option = + let mapToOrganizationForMemberForMember (memberInDb : Member_In_Db) = + let organizationStats = x |> organizationInDb_To_Organization + { OrganizationForMember.Id = x.Id + Name = x.Name + MeAsAMember = memberInDb |> memberInDb_To_MeAsAMember + OtherMembers = organizationStats.Members |> List.filter(fun m -> m.Id <> memberInDb.Id) + ChatRooms = organizationStats.ChatRooms } + FakePersistence.Members.Values + |> Seq.tryFind (fun m -> m.Id = memberId) + |> Option.map mapToOrganizationForMemberForMember + + +module Schema = + open FSharp.Data.GraphQL.Server.AppInfrastructure.Rop + + let validationException_Member_With_This_Name_Already_Exists (theName : string) = + GraphQLException(sprintf "member with name \"%s\" already exists" theName) + + let validationException_Organization_Doesnt_Exist (theId : OrganizationId) = + match theId with + | OrganizationId x -> + GraphQLException (sprintf "organization with ID \"%s\" doesn't exist" (x.ToString())) + + let validationException_ChatRoom_Doesnt_Exist (theId : ChatRoomId) = + GraphQLException(sprintf "chat room with ID \"%s\" doesn't exist" (theId.ToString())) + + let validationException_PrivMember_Doesnt_Exist (theId : MemberPrivateId) = + match theId with + | MemberPrivateId x -> + GraphQLException(sprintf "member with private ID \"%s\" doesn't exist" (x.ToString())) + + let validationException_Member_Isnt_Part_Of_Org () = + GraphQLException("this member is not part of this organization") + + let validationException_ChatRoom_Isnt_Part_Of_Org () = + GraphQLException("this chat room is not part of this organization") + + let authenticateMemberInOrganization (organizationId : OrganizationId) (memberPrivId : MemberPrivateId) : RopResult<(Organization_In_Db * Member_In_Db), GraphQLException> = + let maybeOrganization = FakePersistence.Organizations |> Map.tryFind organizationId + let maybeMember = FakePersistence.Members.Values |> Seq.tryFind (fun x -> x.PrivId = memberPrivId) + + match (maybeOrganization, maybeMember) with + | None, _ -> + fail <| (organizationId |> validationException_Organization_Doesnt_Exist) + | _, None -> + fail <| (memberPrivId |> validationException_PrivMember_Doesnt_Exist) + | Some organization, Some theMember -> + if not (organization.Members |> List.contains theMember.Id) then + fail <| (validationException_Member_Isnt_Part_Of_Org()) + else + succeed (organization, theMember) + + let validateChatRoomExistence (organization : Organization_In_Db) (chatRoomId : ChatRoomId) : RopResult = + match FakePersistence.ChatRooms |> Map.tryFind chatRoomId with + | None -> + fail <| validationException_ChatRoom_Doesnt_Exist chatRoomId + | Some chatRoom -> + if not (organization.ChatRooms |> List.contains chatRoom.Id) then + fail <| validationException_ChatRoom_Isnt_Part_Of_Org() + else + succeed <| chatRoom + + let validateMessageExistence (chatRoom : ChatRoom_In_Db) (messageId : MessageId) : RopResult = + match FakePersistence.ChatRoomMessages |> Map.tryFind (chatRoom.Id, messageId) with + | None -> + fail (GraphQLException("chat message doesn't exist (anymore)")) + | Some chatMessage -> + succeed chatMessage + + let succeedOrRaiseGraphQLEx<'T> (ropResult : RopResult<'T, GraphQLException>) : 'T = + match ropResult with + | Failure exs -> + let firstEx = exs |> List.head + raise firstEx + | Success (s, _) -> + s + + let chatRoomEvents_subscription_name = "chatRoomEvents" + + let memberRoleInChatEnumDef = + Define.Enum( + name = nameof MemberRoleInChat, + options = [ + Define.EnumValue(ChatAdmin.ToString(), ChatAdmin) + Define.EnumValue(ChatGuest.ToString(), ChatGuest) + ] + ) + + let memberDef = + Define.Object( + name = nameof Member, + description = "An organization member", + isTypeOf = (fun o -> o :? Member), + fieldsFn = fun () -> [ + Define.Field("id", SchemaDefinitions.Guid, "the member's ID", fun _ (x : Member) -> match x.Id with MemberId theId -> theId) + Define.Field("name", SchemaDefinitions.String, "the member's name", fun _ (x : Member) -> x.Name) + ] + ) + + let meAsAMemberDef = + Define.Object( + name = nameof MeAsAMember, + description = "An organization member", + isTypeOf = (fun o -> o :? MeAsAMember), + fieldsFn = fun () -> [ + Define.Field("privId", SchemaDefinitions.Guid, "the member's private ID used for authenticating their requests", fun _ (x : MeAsAMember) -> match x.PrivId with MemberPrivateId theId -> theId) + Define.Field("id", SchemaDefinitions.Guid, "the member's ID", fun _ (x : MeAsAMember) -> match x.Id with MemberId theId -> theId) + Define.Field("name", SchemaDefinitions.String, "the member's name", fun _ (x : MeAsAMember) -> x.Name) + ] + ) + + let chatMemberDef = + Define.Object( + name = nameof ChatMember, + description = "A chat member is an organization member participating in a chat room", + isTypeOf = (fun o -> o :? ChatMember), + fieldsFn = fun () -> [ + Define.Field("id", SchemaDefinitions.Guid, "the member's ID", fun _ (x : ChatMember) -> match x.Id with MemberId theId -> theId) + Define.Field("name", SchemaDefinitions.String, "the member's name", fun _ (x : ChatMember) -> x.Name) + Define.Field("role", memberRoleInChatEnumDef, "the member's role in the chat", fun _ (x : ChatMember) -> x.Role) + ] + ) + + let meAsAChatMemberDef = + Define.Object( + name = nameof MeAsAChatMember, + description = "A chat member is an organization member participating in a chat room", + isTypeOf = (fun o -> o :? MeAsAChatMember), + fieldsFn = fun () -> [ + Define.Field("privId", SchemaDefinitions.Guid, "the member's private ID used for authenticating their requests", fun _ (x : MeAsAChatMember) -> match x.PrivId with MemberPrivateId theId -> theId) + Define.Field("id", SchemaDefinitions.Guid, "the member's ID", fun _ (x : MeAsAChatMember) -> match x.Id with MemberId theId -> theId) + Define.Field("name", SchemaDefinitions.String, "the member's name", fun _ (x : MeAsAChatMember) -> x.Name) + Define.Field("role", memberRoleInChatEnumDef, "the member's role in the chat", fun _ (x : MeAsAChatMember) -> x.Role) + ] + ) + + let chatRoomStatsDef = + Define.Object( + name = nameof ChatRoom, + description = "A chat room as viewed from the outside", + isTypeOf = (fun o -> o :? ChatRoom), + fieldsFn = fun () -> [ + Define.Field("id", SchemaDefinitions.Guid, "the chat room's ID", fun _ (x : ChatRoom) -> match x.Id with ChatRoomId theId -> theId) + Define.Field("name", SchemaDefinitions.String, "the chat room's name", fun _ (x : ChatRoom) -> x.Name) + Define.Field("members", ListOf chatMemberDef, "the members in the chat room", fun _ (x : ChatRoom) -> x.Members) + ] + ) + + let chatRoomDetailsDef = + Define.Object( + name = nameof ChatRoomForMember, + description = "A chat room as viewed by a chat room member", + isTypeOf = (fun o -> o :? ChatRoomForMember), + fieldsFn = fun () -> [ + Define.Field("id", SchemaDefinitions.Guid, "the chat room's ID", fun _ (x : ChatRoomForMember) -> match x.Id with ChatRoomId theId -> theId) + Define.Field("name", SchemaDefinitions.String, "the chat room's name", fun _ (x : ChatRoomForMember) -> x.Name) + Define.Field("meAsAChatMember", meAsAChatMemberDef, "the chat member that queried the details", fun _ (x : ChatRoomForMember) -> x.MeAsAChatMember) + Define.Field("otherChatMembers", ListOf chatMemberDef, "the chat members excluding the one who queried the details", fun _ (x : ChatRoomForMember) -> x.OtherChatMembers) + ] + ) + + let organizationStatsDef = + Define.Object( + name = nameof Organization, + description = "An organization as seen from the outside", + isTypeOf = (fun o -> o :? Organization), + fieldsFn = fun () -> [ + Define.Field("id", SchemaDefinitions.Guid, "the organization's ID", fun _ (x : Organization) -> match x.Id with OrganizationId theId -> theId) + Define.Field("name", SchemaDefinitions.String, "the organization's name", fun _ (x : Organization) -> x.Name) + Define.Field("members", ListOf memberDef, "members of this organization", fun _ (x : Organization) -> x.Members) + Define.Field("chatRooms", ListOf chatRoomStatsDef, "chat rooms in this organization", fun _ (x : Organization) -> x.ChatRooms) + ] + ) + + let organizationDetailsDef = + Define.Object( + name = nameof OrganizationForMember, + description = "An organization as seen by one of the organization's members", + isTypeOf = (fun o -> o :? OrganizationForMember), + fieldsFn = fun () -> [ + Define.Field("id", SchemaDefinitions.Guid, "the organization's ID", fun _ (x : OrganizationForMember) -> match x.Id with OrganizationId theId -> theId) + Define.Field("name", SchemaDefinitions.String, "the organization's name", fun _ (x : OrganizationForMember) -> x.Name) + Define.Field("meAsAMember", meAsAMemberDef, "the member that queried the details", fun _ (x : OrganizationForMember) -> x.MeAsAMember) + Define.Field("otherMembers", ListOf memberDef, "members of this organization", fun _ (x : OrganizationForMember) -> x.OtherMembers) + Define.Field("chatRooms", ListOf chatRoomStatsDef, "chat rooms in this organization", fun _ (x : OrganizationForMember) -> x.ChatRooms) + ] + ) + + let aChatRoomMessageDef description name = + Define.Object( + name = name, + description = description, + isTypeOf = (fun o -> o :? ChatRoomMessage), + fieldsFn = fun () -> [ + Define.Field("id", SchemaDefinitions.Guid, "the message's ID", fun _ (x : ChatRoomMessage) -> match x.Id with MessageId theId -> theId) + Define.Field("chatRoomId", SchemaDefinitions.Guid, "the ID of the chat room the message belongs to", fun _ (x : ChatRoomMessage) -> match x.ChatRoomId with ChatRoomId theId -> theId) + Define.Field("date", SchemaDefinitions.Date, "the time the message was received at the server", fun _ (x : ChatRoomMessage) -> x.Date) + Define.Field("authorId", SchemaDefinitions.Guid, "the member ID of the message's author", fun _ (x : ChatRoomMessage) -> match x.AuthorId with MemberId theId -> theId) + Define.Field("text", SchemaDefinitions.String, "the message's text", fun _ (x : ChatRoomMessage) -> x.Text) + ] + ) + + let anEmptyChatRoomEvent description name = + Define.Object( + name = name, + description = description, + isTypeOf = (fun o -> o :? unit), + fieldsFn = fun () -> [ + Define.Field("doNotUse", SchemaDefinitions.Boolean, "this is just to satify the expected structure of this type", fun _ _ -> true) + ] + ) + + let aChatRoomEventForMessageId description name = + Define.Object( + name = name, + description = description, + isTypeOf = (fun o -> o :? MessageId), + fieldsFn = (fun () -> [ + Define.Field("messageId", SchemaDefinitions.Guid, "this is the message ID", fun _ (x : MessageId) -> match x with MessageId theId -> theId) + ]) + ) + + let aChatRoomEventForMemberIdAndName description name = + Define.Object( + name = name, + description = description, + isTypeOf = (fun o -> o :? (MemberId * string)), + fieldsFn = (fun () -> [ + Define.Field("memberId", SchemaDefinitions.Guid, "this is the member's ID", fun _ (mId : MemberId, _ : string) -> match mId with MemberId theId -> theId) + Define.Field("memberName", SchemaDefinitions.String, "this is the member's name", fun _ (_ : MemberId, name : string) -> name) + ]) + ) + + let newMessageDef = nameof NewMessage |> aChatRoomMessageDef "a new public message has been sent in the chat room" + let editedMessageDef = nameof EditedMessage |> aChatRoomMessageDef "a public message of the chat room has been edited" + let deletedMessageDef = nameof DeletedMessage |> aChatRoomEventForMessageId "a public message of the chat room has been deleted" + let memberJoinedDef = nameof MemberJoined |> aChatRoomEventForMemberIdAndName "a member has joined the chat" + let memberLeftDef = nameof MemberLeft |> aChatRoomEventForMemberIdAndName "a member has left the chat" + + let chatRoomSpecificEventDef = + Define.Union( + name = nameof ChatRoomSpecificEvent, + options = + [ newMessageDef + editedMessageDef + deletedMessageDef + memberJoinedDef + memberLeftDef ], + resolveValue = + (fun o -> + match o with + | NewMessage x -> box x + | EditedMessage x -> upcast x + | DeletedMessage x -> upcast x + | MemberJoined (mId, mName) -> upcast (mId, mName) + | MemberLeft (mId, mName) -> upcast (mId, mName) + ), + resolveType = + (fun o -> + match o with + | NewMessage _ -> newMessageDef + | EditedMessage _ -> editedMessageDef + | DeletedMessage _ -> deletedMessageDef + | MemberJoined _ -> memberJoinedDef + | MemberLeft _ -> memberLeftDef + ), + description = "data which is specific to a certain type of event" + ) + + let chatRoomEventDef = + Define.Object( + name = nameof ChatRoomEvent, + description = "Something that happened in the chat room, like a new message sent", + isTypeOf = (fun o -> o :? ChatRoomEvent), + fieldsFn = (fun () -> [ + Define.Field("chatRoomId", SchemaDefinitions.Guid, "the ID of the chat room in which the event happened", fun _ (x : ChatRoomEvent) -> match x.ChatRoomId with ChatRoomId theId -> theId) + Define.Field("time", SchemaDefinitions.Date, "the time the message was received at the server", fun _ (x : ChatRoomEvent) -> x.Time) + Define.Field("specificData", chatRoomSpecificEventDef, "the event's specific data", fun _ (x : ChatRoomEvent) -> x.SpecificData) + ]) + ) + + let query = + Define.Object( + name = "Query", + fields = [ + Define.Field( + "organizations", + ListOf organizationStatsDef, + "gets all available organizations", + fun _ _ -> + FakePersistence.Organizations.Values + |> Seq.map MapFrom.organizationInDb_To_Organization + |> List.ofSeq + ) + ] + ) + + let schemaConfig = SchemaConfig.Default + + let publishChatRoomEvent (specificEvent : ChatRoomSpecificEvent) (chatRoomId : ChatRoomId) : unit = + { ChatRoomId = chatRoomId + Time = DateTime.UtcNow + SpecificData = specificEvent } + |> schemaConfig.SubscriptionProvider.Publish chatRoomEvents_subscription_name + + let mutation = + Define.Object( + name = "Mutation", + fields = [ + Define.Field( + "enterOrganization", + organizationDetailsDef, + "makes a new member enter an organization", + [ Define.Input ("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization") + Define.Input ("member", SchemaDefinitions.String, description = "the new member's name") + ], + fun ctx root -> + let organizationId = OrganizationId (ctx.Arg("organizationId")) + let newMemberName : string = ctx.Arg("member") + let maybeResult = + FakePersistence.Organizations + |> Map.tryFind organizationId + |> Option.map MapFrom.organizationInDb_To_Organization + |> Option.map + (fun organization -> + if organization.Members |> List.exists (fun m -> m.Name = newMemberName) then + raise (newMemberName |> validationException_Member_With_This_Name_Already_Exists) + else + let newMemberPrivId = MemberPrivateId (Guid.NewGuid()) + let newMemberId = MemberId (Guid.NewGuid()) + let newMember = + { Member_In_Db.PrivId = newMemberPrivId + Id = newMemberId + Name = newMemberName } + FakePersistence.Members <- + FakePersistence.Members + |> Map.add newMemberId newMember + + FakePersistence.Organizations <- + FakePersistence.Organizations + |> Map.change organizationId + (Option.bind + (fun organization -> + Some { organization with Members = newMemberId :: organization.Members } + ) + ) + FakePersistence.Organizations + |> Map.find organizationId + |> MapFrom.organizationInDb_To_OrganizationForMember newMemberId + ) + |> Option.flatten + match maybeResult with + | None -> + raise (GraphQLException("couldn't enter organization (maybe the ID is incorrect?)")) + | Some res -> + res + ) + Define.Field( + "createChatRoom", + chatRoomDetailsDef, + "creates a new chat room for a user", + [ Define.Input ("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization in which the chat room will be created") + Define.Input ("memberId", SchemaDefinitions.Guid, description = "the member's private ID") + Define.Input ("name", SchemaDefinitions.String, description = "the chat room's name") + ], + fun ctx root -> + let organizationId = OrganizationId (ctx.Arg("organizationId")) + let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) + let chatRoomName : string = ctx.Arg("name") + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> mapR + (fun (organization, theMember) -> + let newChatRoomId = ChatRoomId (Guid.NewGuid()) + let newChatMember : ChatMember_In_Db = + { ChatRoomId = newChatRoomId + MemberId = theMember.Id + Role = ChatAdmin } + let newChatRoom : ChatRoom_In_Db = + { Id = newChatRoomId + Name = chatRoomName + Members = [ theMember.Id ] } + FakePersistence.ChatRooms <- + FakePersistence.ChatRooms |> Map.add newChatRoomId newChatRoom + FakePersistence.ChatMembers <- + FakePersistence.ChatMembers |> Map.add (newChatRoomId, theMember.Id) newChatMember + FakePersistence.Organizations <- + FakePersistence.Organizations + |> Map.change + organizationId + (Option.map(fun org -> { org with ChatRooms = newChatRoomId :: org.ChatRooms })) + + MapFrom.chatRoomInDb_To_ChatRoomForMember + (FakePersistence.Members.Values |> Seq.filter (fun x -> organization.Members |> List.contains x.Id)) + newChatMember + newChatRoom + ) + |> succeedOrRaiseGraphQLEx + ) + Define.Field( + "enterChatRoom", + chatRoomDetailsDef, + "makes a member enter a chat room", + [ Define.Input ("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization the chat room and member are in") + Define.Input ("chatRoomId", SchemaDefinitions.Guid, description = "the ID of the chat room") + Define.Input ("memberId", SchemaDefinitions.Guid, description = "the member's private ID") + ], + fun ctx root -> + let organizationId = OrganizationId (ctx.Arg("organizationId")) + let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) + let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> bindR + (fun (organization, theMember) -> + chatRoomId + |> validateChatRoomExistence organization + |> mapR (fun chatRoom -> (organization, chatRoom, theMember)) + ) + |> mapR + (fun (_, chatRoom, theMember) -> + let newChatMember : ChatMember_In_Db = + { ChatRoomId = chatRoom.Id + MemberId = theMember.Id + Role = ChatGuest + } + FakePersistence.ChatMembers <- + FakePersistence.ChatMembers + |> Map.add + (newChatMember.ChatRoomId, newChatMember.MemberId) + newChatMember + FakePersistence.ChatRooms <- + FakePersistence.ChatRooms + |> Map.change + chatRoom.Id + (Option.map (fun theChatRoom -> { theChatRoom with Members = newChatMember.MemberId :: theChatRoom.Members})) + let theChatRoom = FakePersistence.ChatRooms |> Map.find chatRoomId + let result = + MapFrom.chatRoomInDb_To_ChatRoomForMember + (FakePersistence.Members.Values + |> Seq.filter + (fun x -> theChatRoom.Members |> List.contains x.Id) + ) + newChatMember + theChatRoom + + chatRoom.Id + |> publishChatRoomEvent (MemberJoined (theMember.Id, theMember.Name)) + + result + ) + |> succeedOrRaiseGraphQLEx + ) + Define.Field( + "leaveChatRoom", + SchemaDefinitions.Boolean, + "makes a member leave a chat room", + [ Define.Input ("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization the chat room and member are in") + Define.Input ("chatRoomId", SchemaDefinitions.Guid, description = "the ID of the chat room") + Define.Input ("memberId", SchemaDefinitions.Guid, description = "the member's private ID") + ], + fun ctx root -> + let organizationId = OrganizationId (ctx.Arg("organizationId")) + let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) + let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> bindR + (fun (organization, theMember) -> + chatRoomId + |> validateChatRoomExistence organization + |> mapR (fun chatRoom -> (organization, chatRoom, theMember)) + ) + |> mapR + (fun (_, chatRoom, theMember) -> + FakePersistence.ChatMembers <- + FakePersistence.ChatMembers |> Map.remove (chatRoom.Id, theMember.Id) + FakePersistence.ChatRooms <- + FakePersistence.ChatRooms + |> Map.change + chatRoom.Id + (Option.map (fun theChatRoom -> { theChatRoom with Members = theChatRoom.Members |> List.filter (fun mId -> mId <> theMember.Id)})) + true + ) + |> succeedOrRaiseGraphQLEx + ) + Define.Field( + "sendChatMessage", + SchemaDefinitions.Boolean, + [ Define.Input("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization the chat room and member are in") + Define.Input("chatRoomId", SchemaDefinitions.Guid, description = "the chat room's ID") + Define.Input("memberId", SchemaDefinitions.Guid, description = "the member's private ID") + Define.Input("text", SchemaDefinitions.String, description = "the chat message's contents")], + fun ctx _ -> + let organizationId = OrganizationId (ctx.Arg("organizationId")) + let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) + let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) + let text : string = ctx.Arg("text") + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> bindR + (fun (organization, theMember) -> + chatRoomId + |> validateChatRoomExistence organization + |> mapR (fun chatRoom -> (organization, chatRoom, theMember)) + ) + |> mapR + (fun (_, chatRoom, theMember) -> + let newChatRoomMessage = + { Id = MessageId (Guid.NewGuid()) + ChatRoomId = chatRoom.Id + Date = DateTime.UtcNow + AuthorId = theMember.Id + Text = text } + FakePersistence.ChatRoomMessages <- + FakePersistence.ChatRoomMessages + |> Map.add + (chatRoom.Id, newChatRoomMessage.Id) + newChatRoomMessage + + chatRoom.Id + |> publishChatRoomEvent (NewMessage newChatRoomMessage) + + true + ) + |> succeedOrRaiseGraphQLEx + ) + Define.Field( + "editChatMessage", + SchemaDefinitions.Boolean, + [ Define.Input("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization the chat room and member are in") + Define.Input("chatRoomId", SchemaDefinitions.Guid, description = "the chat room's ID") + Define.Input("memberId", SchemaDefinitions.Guid, description = "the member's private ID") + Define.Input("messageId", SchemaDefinitions.Guid, description = "the existing message's ID") + Define.Input("text", SchemaDefinitions.String, description = "the chat message's contents")], + fun ctx _ -> + let organizationId = OrganizationId (ctx.Arg("organizationId")) + let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) + let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) + let messageId = MessageId (ctx.Arg("messageId")) + let text : string = ctx.Arg("text") + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> bindR + (fun (organization, theMember) -> + chatRoomId + |> validateChatRoomExistence organization + |> bindR (fun chatRoom -> messageId |> validateMessageExistence chatRoom |> mapR (fun x -> (chatRoom, x))) + |> mapR (fun (chatRoom, chatMessage) -> (organization, chatRoom, theMember, chatMessage)) + ) + |> mapR + (fun (_, chatRoom, theMember, chatMessage) -> + let newChatRoomMessage = + { Id = chatMessage.Id + ChatRoomId = chatRoom.Id + Date = chatMessage.Date + AuthorId = theMember.Id + Text = text } + FakePersistence.ChatRoomMessages <- + FakePersistence.ChatRoomMessages + |> Map.change + (chatRoom.Id, newChatRoomMessage.Id) + (Option.map (fun _ -> newChatRoomMessage)) + + chatRoom.Id + |> publishChatRoomEvent (EditedMessage newChatRoomMessage) + + true + ) + |> succeedOrRaiseGraphQLEx + ) + Define.Field( + "deleteChatMessage", + SchemaDefinitions.Boolean, + [ Define.Input("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization the chat room and member are in") + Define.Input("chatRoomId", SchemaDefinitions.Guid, description = "the chat room's ID") + Define.Input("memberId", SchemaDefinitions.Guid, description = "the member's private ID") + Define.Input("messageId", SchemaDefinitions.Guid, description = "the existing message's ID")], + fun ctx _ -> + let organizationId = OrganizationId (ctx.Arg("organizationId")) + let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) + let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) + let messageId = MessageId (ctx.Arg("messageId")) + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> bindR + (fun (organization, theMember) -> + chatRoomId + |> validateChatRoomExistence organization + |> bindR (fun chatRoom -> messageId |> validateMessageExistence chatRoom |> mapR (fun x -> (chatRoom, x))) + |> mapR (fun (chatRoom, chatMessage) -> (organization, chatRoom, theMember, chatMessage)) + ) + |> mapR + (fun (_, chatRoom, theMember, chatMessage) -> + FakePersistence.ChatRoomMessages <- + FakePersistence.ChatRoomMessages + |> Map.remove (chatRoom.Id, chatMessage.Id) + + chatRoom.Id + |> publishChatRoomEvent (DeletedMessage chatMessage.Id) + + true + ) + |> succeedOrRaiseGraphQLEx + ) + ] + ) + + let rootDef = + Define.Object( + name = "Root", + description = "contains general request information", + isTypeOf = (fun o -> o :? Root), + fieldsFn = fun () -> + [ Define.Field("requestId", SchemaDefinitions.String, "The request's unique ID.", fun _ (r : Root) -> r.RequestId) ] + ) + + let subscription = + Define.SubscriptionObject( + name = "Subscription", + fields = [ + Define.SubscriptionField( + chatRoomEvents_subscription_name, + rootDef, + chatRoomEventDef, + "events related to a specific chat room", + [ Define.Input("chatRoomId", SchemaDefinitions.Guid, description = "the ID of the chat room to listen to events from") + Define.Input("memberId", SchemaDefinitions.Guid, description = "the member's private ID")], + (fun ctx _ (chatRoomEvent : ChatRoomEvent) -> + let chatRoomIdOfInterest = ChatRoomId (ctx.Arg("chatRoomId")) + let memberId = MemberPrivateId (ctx.Arg("memberId")) + + if chatRoomEvent.ChatRoomId <> chatRoomIdOfInterest then + None + else + let chatRoom = FakePersistence.ChatRooms |> Map.find chatRoomEvent.ChatRoomId + let chatRoomMembersPrivIds = + FakePersistence.Members.Values + |> Seq.filter (fun m -> chatRoom.Members |> List.contains m.Id) + |> Seq.map (fun m -> m.PrivId) + if not (chatRoomMembersPrivIds |> Seq.contains memberId) then + None + else + Some chatRoomEvent + ) + ) + ] + ) + + let schema : ISchema = Schema(query, mutation, subscription, schemaConfig) + + let executor = Executor(schema, []) \ No newline at end of file diff --git a/samples/chat-app/server/appsettings.Development.json b/samples/chat-app/server/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/samples/chat-app/server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/chat-app/server/appsettings.json b/samples/chat-app/server/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/samples/chat-app/server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/chat-app/server/chat-app.fsproj b/samples/chat-app/server/chat-app.fsproj new file mode 100644 index 000000000..de8125856 --- /dev/null +++ b/samples/chat-app/server/chat-app.fsproj @@ -0,0 +1,18 @@ + + + + net6.0 + chat_app + + + + + + + + + + + + + diff --git a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj index 9e9fe1459..5e9073532 100644 --- a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj +++ b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj @@ -16,12 +16,7 @@ - - - - - @@ -30,6 +25,7 @@ + diff --git a/samples/star-wars-api/Helpers.fs b/samples/star-wars-api/Helpers.fs deleted file mode 100644 index 182934801..000000000 --- a/samples/star-wars-api/Helpers.fs +++ /dev/null @@ -1,45 +0,0 @@ -namespace FSharp.Data.GraphQL.Samples.StarWarsApi - -open System -open System.Text -open Newtonsoft.Json -open Newtonsoft.Json.Linq -open Newtonsoft.Json.Serialization -open System.Collections.Generic - -[] -module Helpers = - let tee f x = - f x - x - -[] -module StringHelpers = - let utf8String (bytes : byte seq) = - bytes - |> Seq.filter (fun i -> i > 0uy) - |> Array.ofSeq - |> Encoding.UTF8.GetString - - let utf8Bytes (str : string) = str |> Encoding.UTF8.GetBytes - - let isNullOrWhiteSpace (str : string) = String.IsNullOrWhiteSpace (str) - -[] -module JsonHelpers = - let tryGetJsonProperty (jobj : JObject) prop = - match jobj.Property (prop) with - | null -> None - | p -> Some (p.Value.ToString ()) - - let jsonSerializerSettings (converters : JsonConverter seq) = - JsonSerializerSettings () - |> tee (fun s -> - s.Converters <- List (converters) - s.ContractResolver <- CamelCasePropertyNamesContractResolver ()) - - let jsonSerializer (converters : JsonConverter seq) = - JsonSerializer () - |> tee (fun c -> - Seq.iter c.Converters.Add converters - c.ContractResolver <- CamelCasePropertyNamesContractResolver ()) diff --git a/samples/star-wars-api/HttpHandlers.fs b/samples/star-wars-api/HttpHandlers.fs deleted file mode 100644 index 1366feeee..000000000 --- a/samples/star-wars-api/HttpHandlers.fs +++ /dev/null @@ -1,115 +0,0 @@ -namespace FSharp.Data.GraphQL.Samples.StarWarsApi - -open System.IO -open System.Text -open Giraffe -open Microsoft.AspNetCore.Http -open Newtonsoft.Json -open Newtonsoft.Json.Linq -open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL -open FSharp.Data.GraphQL.Types - -type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult - -module HttpHandlers = - - let private converters : JsonConverter[] = [| OptionConverter () |] - let private jsonSettings = jsonSerializerSettings converters - - let internalServerError : HttpHandler = setStatusCode 500 - - let okWithStr str : HttpHandler = setStatusCode 200 >=> text str - - let setCorsHeaders : HttpHandler = - setHttpHeader "Access-Control-Allow-Origin" "*" - >=> setHttpHeader "Access-Control-Allow-Headers" "content-type" - - let setContentTypeAsJson : HttpHandler = setHttpHeader "Content-Type" "application/json" - - // TODO: Rewrite completely to async code - let private graphQL (next : HttpFunc) (ctx : HttpContext) = task { - let serialize d = JsonConvert.SerializeObject (d, jsonSettings) - - let deserialize (data : string) = - let getMap (token : JToken) = - let rec mapper (name : string) (token : JToken) = - match name, token.Type with - | "variables", JTokenType.Object -> token.Children () |> Seq.map (fun x -> x.Name, mapper x.Name x.Value) |> Map.ofSeq |> box - | name , JTokenType.Array -> token |> Seq.map (fun x -> mapper name x) |> Array.ofSeq |> box - | _ -> (token :?> JValue).Value - - token.Children () - |> Seq.map (fun x -> x.Name, mapper x.Name x.Value) - |> Map.ofSeq - - if System.String.IsNullOrWhiteSpace (data) - then None - else data |> JToken.Parse |> getMap |> Some - - let json = - function - | Direct (data, _) -> - JsonConvert.SerializeObject (data, jsonSettings) - | Deferred (data, _, deferred) -> - deferred |> Observable.add (fun d -> printfn "Deferred: %s" (serialize d)) - JsonConvert.SerializeObject (data, jsonSettings) - | Stream data -> - data |> Observable.add (fun d -> printfn "Subscription data: %s" (serialize d)) - "{}" - - let removeWhitespacesAndLineBreaks (str : string) = str.Trim().Replace ("\r\n", " ") - - let readStream (s : Stream) = - use ms = new MemoryStream (4096) - s.CopyTo (ms) - ms.ToArray () - - let data = Encoding.UTF8.GetString (readStream ctx.Request.Body) |> deserialize - - let query = - data - |> Option.bind (fun data -> - if data.ContainsKey ("query") - then - match data.["query"] with - | :? string as x -> Some x - | _ -> failwith "Failure deserializing repsonse. Could not read query - it is not stringified in request." - else - None) - - let variables = - data - |> Option.bind (fun data -> - if data.ContainsKey ("variables") - then - match data.["variables"] with - | null -> None - | :? string as x -> deserialize x - | :? Map as x -> Some x - | _ -> failwith "Failure deserializing response. Could not read variables - it is not a object in the request." - else - None) - - match query, variables with - | Some query, Some variables -> - printfn "Received query: %s" query - printfn "Received variables: %A" variables - let query = removeWhitespacesAndLineBreaks query - let root = { RequestId = System.Guid.NewGuid().ToString () } - let result = Schema.executor.AsyncExecute (query, root, variables) |> Async.RunSynchronously - printfn "Result metadata: %A" result.Metadata - return! okWithStr (json result) next ctx - | Some query, None -> - printfn "Received query: %s" query - let query = removeWhitespacesAndLineBreaks query - let result = Schema.executor.AsyncExecute (query) |> Async.RunSynchronously - printfn "Result metadata: %A" result.Metadata - return! okWithStr (json result) next ctx - | None, _ -> - let result = Schema.executor.AsyncExecute (Introspection.IntrospectionQuery) |> Async.RunSynchronously - printfn "Result metadata: %A" result.Metadata - return! okWithStr (json result) next ctx - } - - let webApp : HttpHandler = setCorsHeaders >=> graphQL >=> setContentTypeAsJson diff --git a/samples/star-wars-api/JsonConverters.fs b/samples/star-wars-api/JsonConverters.fs deleted file mode 100644 index 15b15001b..000000000 --- a/samples/star-wars-api/JsonConverters.fs +++ /dev/null @@ -1,175 +0,0 @@ -namespace FSharp.Data.GraphQL.Samples.StarWarsApi - -open Newtonsoft.Json -open Newtonsoft.Json.Linq -open Microsoft.FSharp.Reflection -open FSharp.Data.GraphQL -open FSharp.Data.GraphQL.Types -open FSharp.Data.GraphQL.Types.Patterns - -[] -type OptionConverter () = - inherit JsonConverter () - - override _.CanConvert (t) = - t.IsGenericType && t.GetGenericTypeDefinition () = typedefof> - - override _.WriteJson (writer, value, serializer) = - - let value = - if isNull value then null - else - let _, fields = Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields (value, value.GetType ()) - fields.[0] - - serializer.Serialize (writer, value) - - override _.ReadJson (reader, t, _, serializer) = - - let innerType = t.GetGenericArguments().[0] - - let innerType = - if innerType.IsValueType - then (typedefof>).MakeGenericType ([| innerType |]) - else innerType - - let value = serializer.Deserialize (reader, innerType) - let cases = FSharpType.GetUnionCases (t) - - if isNull value - then FSharpValue.MakeUnion (cases.[0], [||]) - else FSharpValue.MakeUnion (cases.[1], [| value |]) - -[] -type GraphQLQueryConverter<'a> (executor : Executor<'a>, replacements : Map, ?meta : Metadata) = - inherit JsonConverter () - - override _.CanConvert (t) = t = typeof - - override _.WriteJson (_, _, _) = failwith "Not supported" - - override _.ReadJson (reader, _, _, serializer) = - - let jobj = JObject.Load reader - let query = jobj.Property("query").Value.ToString () - - let plan = - match meta with - | Some meta -> executor.CreateExecutionPlan (query, meta = meta) - | None -> executor.CreateExecutionPlan (query) - - let varDefs = plan.Variables - - match varDefs with - | [] -> upcast { ExecutionPlan = plan; Variables = Map.empty } - | vs -> - // For multipart requests, we need to replace some variables - Map.iter (fun path rep -> jobj.SelectToken(path).Replace (JObject.FromObject (rep))) replacements - let vars = JObject.Parse (jobj.Property("variables").Value.ToString ()) - - let variables = - vs - |> List.fold - (fun (acc : Map) (vdef : VarDef) -> - match vars.TryGetValue (vdef.Name) with - | true, jval -> - let v = - match jval.Type with - | JTokenType.Null -> null - | JTokenType.String -> jval.ToString () :> obj - | _ -> jval.ToObject (vdef.TypeDef.Type, serializer) - - Map.add (vdef.Name) v acc - | false, _ -> - match vdef.DefaultValue, vdef.TypeDef with - | Some _, _ -> acc - | _, Nullable _ -> acc - | None, _ -> failwithf "Variable %s has no default value and is missing!" vdef.Name) - Map.empty - - upcast { ExecutionPlan = plan; Variables = variables } - -[] -type WebSocketClientMessageConverter<'a> (executor : Executor<'a>, replacements : Map, ?meta : Metadata) = - inherit JsonConverter () - - override _.CanWrite = false - - override _.CanConvert (t) = t = typeof - - override _.WriteJson (_, _, _) = failwith "Not supported" - - override _.ReadJson (reader, _, _, _) = - - let jobj = JObject.Load reader - let typ = jobj.Property("type").Value.ToString () - - match typ with - | "connection_init" -> upcast ConnectionInit - | "connection_terminate" -> upcast ConnectionTerminate - | "start" -> - let id = tryGetJsonProperty jobj "id" - let payload = tryGetJsonProperty jobj "payload" - - match id, payload with - | Some id, Some payload -> - try - let settings = JsonSerializerSettings () - - let queryConverter = - match meta with - | Some meta -> GraphQLQueryConverter (executor, replacements, meta) :> JsonConverter - | None -> GraphQLQueryConverter (executor, replacements) :> JsonConverter - - let optionConverter = OptionConverter () :> JsonConverter - settings.Converters <- [| optionConverter; queryConverter |] - settings.ContractResolver <- Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver () - let req = JsonConvert.DeserializeObject (payload, settings) - upcast Start (id, req) - with e -> - upcast ParseError (Some id, "Parse Failed with Exception: " + e.Message) - | None, _ -> upcast ParseError (None, "Malformed GQL_START message, expected id field but found none") - | _, None -> upcast ParseError (None, "Malformed GQL_START message, expected payload field but found none") - | "stop" -> - match tryGetJsonProperty jobj "id" with - | Some id -> upcast Stop (id) - | None -> upcast ParseError (None, "Malformed GQL_STOP message, expected id field but found none") - | typ -> upcast ParseError (None, "Message Type " + typ + " is not supported!") - -[] -type WebSocketServerMessageConverter () = - inherit JsonConverter () - - override _.CanRead = false - - override _.CanConvert (t) = - t = typedefof || t.DeclaringType = typedefof - - override _.WriteJson (writer, value, _) = - let value = value :?> WebSocketServerMessage - let jobj = JObject () - - match value with - | ConnectionAck -> jobj.Add (JProperty ("type", "connection_ack")) - | ConnectionError (err) -> - let errObj = JObject () - errObj.Add (JProperty ("error", err)) - jobj.Add (JProperty ("type", "connection_error")) - jobj.Add (JProperty ("payload", errObj)) - | Error (id, err) -> - let errObj = JObject () - errObj.Add (JProperty ("error", err)) - jobj.Add (JProperty ("type", "error")) - jobj.Add (JProperty ("payload", errObj)) - jobj.Add (JProperty ("id", id)) - | Data (id, result) -> - jobj.Add (JProperty ("type", "data")) - jobj.Add (JProperty ("id", id)) - jobj.Add (JProperty ("payload", JObject.FromObject (result))) - | Complete (id) -> - jobj.Add (JProperty ("type", "complete")) - jobj.Add (JProperty ("id", id)) - - jobj.WriteTo (writer) - - override _.ReadJson (_, _, _, _) = failwith "Not supported" diff --git a/samples/star-wars-api/Startup.fs b/samples/star-wars-api/Startup.fs index 3c5a97a27..6601da30f 100644 --- a/samples/star-wars-api/Startup.fs +++ b/samples/star-wars-api/Startup.fs @@ -1,16 +1,23 @@ namespace FSharp.Data.GraphQL.Samples.StarWarsApi -open System +open Giraffe +open FSharp.Data.GraphQL.Server.AppInfrastructure.Giraffe +open FSharp.Data.GraphQL.Server.AppInfrastructure open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Server.Kestrel.Core open Microsoft.Extensions.Configuration open Microsoft.Extensions.DependencyInjection -open Microsoft.Extensions.Hosting open Microsoft.Extensions.Logging -open Giraffe +open System +open Microsoft.AspNetCore.Server.Kestrel.Core +open Microsoft.Extensions.Hosting type Startup private () = + + let rootFactory () : Root = + { RequestId = Guid.NewGuid().ToString() } + new (configuration: IConfiguration) as this = Startup() then this.Configuration <- configuration @@ -19,9 +26,14 @@ type Startup private () = services.AddGiraffe() .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) + .AddGraphQLOptions( + Schema.executor, + rootFactory, + "/ws" + ) |> ignore - member _.Configure(app: IApplicationBuilder, env: IHostEnvironment) = + member _.Configure(app: IApplicationBuilder, env: IHostEnvironment, applicationLifetime : IHostApplicationLifetime, loggerFactory : ILoggerFactory) = let errorHandler (ex : Exception) (log : ILogger) = log.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.") clearResponse >=> setStatusCode 500 @@ -35,7 +47,11 @@ type Startup private () = app .UseGiraffeErrorHandler(errorHandler) .UseWebSockets() - .UseMiddleware>(Schema.executor, fun () -> { RequestId = Guid.NewGuid().ToString() }) - .UseGiraffe HttpHandlers.webApp + .UseWebSocketsForGraphQL() + .UseGiraffe (HttpHandlers.handleGraphQL + applicationLifetime.ApplicationStopping + (loggerFactory.CreateLogger("HttpHandlers.handlerGraphQL")) + ) + member val Configuration : IConfiguration = null with get, set diff --git a/samples/star-wars-api/WebSocketMessages.fs b/samples/star-wars-api/WebSocketMessages.fs deleted file mode 100644 index 258aaba58..000000000 --- a/samples/star-wars-api/WebSocketMessages.fs +++ /dev/null @@ -1,22 +0,0 @@ -namespace FSharp.Data.GraphQL.Samples.StarWarsApi - -open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL.Types - -type GraphQLQuery = - { ExecutionPlan : ExecutionPlan - Variables : Map } - -type WebSocketClientMessage = - | ConnectionInit - | ConnectionTerminate - | Start of id : string * payload : GraphQLQuery - | Stop of id : string - | ParseError of id : string option * err : string - -type WebSocketServerMessage = - | ConnectionAck - | ConnectionError of err : string - | Data of id : string * payload : Output - | Error of id : string option * err : string - | Complete of id : string diff --git a/samples/star-wars-api/WebSocketMiddleware.fs b/samples/star-wars-api/WebSocketMiddleware.fs deleted file mode 100644 index ed379c573..000000000 --- a/samples/star-wars-api/WebSocketMiddleware.fs +++ /dev/null @@ -1,157 +0,0 @@ -namespace FSharp.Data.GraphQL.Samples.StarWarsApi - -open System -open System.Threading -open System.Threading.Tasks -open System.Net.WebSockets -open Microsoft.AspNetCore.Http -open Newtonsoft.Json -open FSharp.Data.GraphQL -open FSharp.Data.GraphQL.Execution -open System.Collections.Generic -open System.Collections.Concurrent - -type GraphQLWebSocket(innerSocket : WebSocket) = - inherit WebSocket() - - let subscriptions = ConcurrentDictionary() :> IDictionary - let id = System.Guid.NewGuid() - - override _.CloseStatus = innerSocket.CloseStatus - - override _.CloseStatusDescription = innerSocket.CloseStatusDescription - - override _.State = innerSocket.State - - override _.SubProtocol = innerSocket.SubProtocol - - override _.CloseAsync(status, description, ct) = innerSocket.CloseAsync(status, description, ct) - - override _.CloseOutputAsync(status, description, ct) = innerSocket.CloseOutputAsync(status, description, ct) - - override this.Dispose() = - this.UnsubscribeAll() - innerSocket.Dispose() - - override _.ReceiveAsync(buffer : ArraySegment, ct) = innerSocket.ReceiveAsync(buffer, ct) - - override _.SendAsync(buffer : ArraySegment, msgType, endOfMsg, ct) = innerSocket.SendAsync(buffer, msgType, endOfMsg, ct) - - override _.Abort() = innerSocket.Abort() - - member _.Subscribe(id : string, unsubscriber : IDisposable) = - subscriptions.Add(id, unsubscriber) - - member _.Unsubscribe(id : string) = - match subscriptions.ContainsKey(id) with - | true -> - subscriptions.[id].Dispose() - subscriptions.Remove(id) |> ignore - | false -> () - - member _.UnsubscribeAll() = - subscriptions - |> Seq.iter (fun x -> x.Value.Dispose()) - subscriptions.Clear() - - member _.Id = id - -module SocketManager = - let private sockets = ConcurrentDictionary() :> IDictionary - - let private disposeSocket (socket : GraphQLWebSocket) = - sockets.Remove(socket.Id) |> ignore - socket.Dispose() - - let private sendMessage (socket : GraphQLWebSocket) (message : WebSocketServerMessage) = async { - let settings = - WebSocketServerMessageConverter() :> JsonConverter - |> Seq.singleton - |> jsonSerializerSettings - let json = JsonConvert.SerializeObject(message, settings) - let buffer = utf8Bytes json - let segment = new ArraySegment(buffer) - if socket.State = WebSocketState.Open then - do! socket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None) |> Async.AwaitTask - else - disposeSocket socket - } - - let private receiveMessage (executor : Executor<'Root>) (replacements : Map) (socket : WebSocket) = async { - let buffer = Array.zeroCreate 4096 - let segment = ArraySegment(buffer) - do! socket.ReceiveAsync(segment, CancellationToken.None) - |> Async.AwaitTask - |> Async.Ignore - let message = utf8String buffer - if isNullOrWhiteSpace message - then - return None - else - let settings = - WebSocketClientMessageConverter(executor, replacements) :> JsonConverter - |> Seq.singleton - |> jsonSerializerSettings - return JsonConvert.DeserializeObject(message, settings) |> Some - } - - let private handleMessages (executor : Executor<'Root>) (root : unit -> 'Root) (socket : GraphQLWebSocket) = async { - let send id output = - Data (id, output) - |> sendMessage socket - |> Async.RunSynchronously - let sendDelayed id output = - Thread.Sleep(5000) - send id output - let handle id = - function - | Stream output -> - let unsubscriber = output |> Observable.subscribe (fun o -> send id o) - socket.Subscribe(id, unsubscriber) - | Deferred (data, _, output) -> - send id data - let unsubscriber = output |> Observable.subscribe (fun o -> sendDelayed id o) - socket.Subscribe(id, unsubscriber) - | Direct (data, _) -> - send id data - try - let mutable loop = true - while loop do - let! message = socket |> receiveMessage executor Map.empty - match message with - | Some ConnectionInit -> - do! sendMessage socket ConnectionAck - | Some (Start (id, payload)) -> - executor.AsyncExecute(payload.ExecutionPlan, root(), payload.Variables) - |> Async.RunSynchronously - |> handle id - do! Data (id, Dictionary()) |> sendMessage socket - | Some ConnectionTerminate -> - do! socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None) |> Async.AwaitTask - disposeSocket socket - loop <- false - | Some (ParseError (id, _)) -> - do! Error (id, "Invalid message type!") |> sendMessage socket - | Some (Stop id) -> - socket.Unsubscribe(id) - do! Complete id |> sendMessage socket - | None -> () - with - | _ -> disposeSocket socket - } - - let startSocket (socket : GraphQLWebSocket) (executor : Executor<'Root>) (root : unit -> 'Root) = - sockets.Add(socket.Id, socket) - handleMessages executor root socket |> Async.RunSynchronously - -type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, executor : Executor<'Root>, root : unit -> 'Root) = - member _.Invoke(ctx : HttpContext) = - async { - match ctx.WebSockets.IsWebSocketRequest with - | true -> - let! socket = ctx.WebSockets.AcceptWebSocketAsync("graphql-ws") |> Async.AwaitTask - use socket = new GraphQLWebSocket(socket) - SocketManager.startSocket socket executor root - | false -> - next.Invoke(ctx) |> ignore - } |> Async.StartAsTask :> Task diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Exceptions.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Exceptions.fs new file mode 100644 index 000000000..7286292ef --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Exceptions.fs @@ -0,0 +1,4 @@ +namespace FSharp.Data.GraphQL.Server.AppInfrastructure + +type InvalidMessageException (explanation : string) = + inherit System.Exception(explanation) \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj new file mode 100644 index 000000000..e493332c4 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj @@ -0,0 +1,36 @@ + + + + net6.0 + true + true + FSharp implementation of Facebook GraphQL query language (Application Infrastructure) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs new file mode 100644 index 000000000..e0b13c03c --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs @@ -0,0 +1,163 @@ +namespace FSharp.Data.GraphQL.Server.AppInfrastructure.Giraffe + +open FSharp.Data.GraphQL.Execution +open FSharp.Data.GraphQL +open Giraffe +open FSharp.Data.GraphQL.Server.AppInfrastructure +open FSharp.Data.GraphQL.Server.AppInfrastructure.Rop +open Microsoft.AspNetCore.Http +open Microsoft.Extensions.Logging +open System.Collections.Generic +open System.Text.Json +open System.Threading +open System.Threading.Tasks + +type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult + +module HttpHandlers = + + let private getRequiredService<'Service> (ctx : HttpContext) : 'Service = + let service = (ctx.GetService<'Service>()) + if obj.ReferenceEquals(service, null) + then + let theType = typedefof<'Service> + failwithf "required service \"%s\" was not registered at the dependency injection container!" (theType.FullName) + service + + let private httpOk (cancellationToken : CancellationToken) (serializerOptions : JsonSerializerOptions) payload : HttpHandler = + setStatusCode 200 + >=> (setHttpHeader "Content-Type" "application/json") + >=> (fun _ ctx -> + JsonSerializer + .SerializeAsync( + ctx.Response.Body, + payload, + options = serializerOptions, + cancellationToken = cancellationToken + ) + .ContinueWith(fun _ -> Some ctx) // what about when serialization fails? Maybe it will never at this stage anyway... + ) + + let private prepareGenericErrors (errorMessages : string list) = + (NameValueLookup.ofList + [ "errors", + upcast + ( errorMessages + |> List.map + (fun msg -> + NameValueLookup.ofList ["message", upcast msg] + ) + ) + ] + ) + + let private addToErrorsInData (theseNew: Error list) (data : IDictionary) = + let toNameValueLookupList (errors : Error list) = + errors + |> List.map + (fun (errMsg, path) -> + NameValueLookup.ofList ["message", upcast errMsg; "path", upcast path] + ) + let result = + data + |> Seq.map(fun x -> (x.Key, x.Value)) + |> Map.ofSeq + match data.TryGetValue("errors") with + | (true, (:? list as nameValueLookups)) -> + result + |> Map.change + "errors" + (fun _ -> Some <| upcast (nameValueLookups @ (theseNew |> toNameValueLookupList) |> List.distinct)) + | (true, _) -> + result + | (false, _) -> + result + |> Map.add + "errors" + (upcast (theseNew |> toNameValueLookupList)) + + let handleGraphQL<'Root> + (cancellationToken : CancellationToken) + (logger : ILogger) + (next : HttpFunc) (ctx : HttpContext) = + task { + let cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ctx.RequestAborted).Token + if cancellationToken.IsCancellationRequested then + return (fun _ -> None) ctx + else + let options = ctx |> getRequiredService> + let executor = options.SchemaExecutor + let rootFactory = options.RootFactory + let serializerOptions = options.SerializerOptions + let deserializeGraphQLRequest () = + try + JsonSerializer.DeserializeAsync( + ctx.Request.Body, + serializerOptions + ).AsTask() + .ContinueWith>(fun (x : Task) -> succeed x.Result) + with + | :? GraphQLException as ex -> + Task.FromResult(fail (sprintf "%s" (ex.Message))) + + let applyPlanExecutionResult (result : GQLResponse) = + task { + match result with + | Direct (data : IDictionary, errs) -> + let finalData = data |> addToErrorsInData errs + return! httpOk cancellationToken serializerOptions finalData next ctx + | other -> + let error = + prepareGenericErrors ["subscriptions are not supported here (use the websocket endpoint instead)."] + return! httpOk cancellationToken serializerOptions error next ctx + } + if ctx.Request.Headers.ContentLength.GetValueOrDefault(0) = 0 then + let! result = executor.AsyncExecute (Introspection.IntrospectionQuery) |> Async.StartAsTask + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) + else + () + return! result |> applyPlanExecutionResult + else + match! deserializeGraphQLRequest() with + | Failure errMsgs -> + return! httpOk cancellationToken serializerOptions (prepareGenericErrors errMsgs) next ctx + | Success (graphqlRequest, _) -> + match graphqlRequest.Query with + | None -> + let! result = executor.AsyncExecute (Introspection.IntrospectionQuery) |> Async.StartAsTask + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) + else + () + return! result |> applyPlanExecutionResult + | Some queryAsStr -> + let graphQLQueryDecodingResult = + queryAsStr + |> GraphQLQueryDecoding.decodeGraphQLQuery + serializerOptions + executor + graphqlRequest.OperationName + graphqlRequest.Variables + match graphQLQueryDecodingResult with + | Failure errMsgs -> + return! + httpOk cancellationToken serializerOptions (prepareGenericErrors errMsgs) next ctx + | Success (query, _) -> + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(sprintf "Received query: %A" query) + else + () + let root = rootFactory() + let! result = + executor.AsyncExecute( + query.ExecutionPlan, + data = root, + variables = query.Variables + )|> Async.StartAsTask + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) + else + () + return! result |> applyPlanExecutionResult + } \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLOptions.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLOptions.fs new file mode 100644 index 000000000..d8c1a4ef3 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLOptions.fs @@ -0,0 +1,21 @@ +namespace FSharp.Data.GraphQL.Server.AppInfrastructure + +open FSharp.Data.GraphQL +open System +open System.Text.Json +open System.Threading.Tasks + +type PingHandler = + IServiceProvider -> JsonDocument option -> Task + +type GraphQLTransportWSOptions = + { EndpointUrl: string + ConnectionInitTimeoutInMs: int + CustomPingHandler : PingHandler option } + +type GraphQLOptions<'Root> = + { SchemaExecutor: Executor<'Root> + RootFactory: unit -> 'Root + SerializerOptions: JsonSerializerOptions + WebsocketOptions: GraphQLTransportWSOptions + } \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLSubscriptionsManagement.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLSubscriptionsManagement.fs new file mode 100644 index 000000000..d3de048f4 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLSubscriptionsManagement.fs @@ -0,0 +1,32 @@ +namespace FSharp.Data.GraphQL.Server.AppInfrastructure + +module internal GraphQLSubscriptionsManagement = + let addSubscription (id : SubscriptionId, unsubscriber : SubscriptionUnsubscriber, onUnsubscribe : OnUnsubscribeAction) + (subscriptions : SubscriptionsDict) = + subscriptions.Add(id, (unsubscriber, onUnsubscribe)) + + let isIdTaken (id : SubscriptionId) (subscriptions : SubscriptionsDict) = + subscriptions.ContainsKey(id) + + let executeOnUnsubscribeAndDispose (id : SubscriptionId) (subscription : SubscriptionUnsubscriber * OnUnsubscribeAction) = + match subscription with + | unsubscriber, onUnsubscribe -> + try + id |> onUnsubscribe + finally + unsubscriber.Dispose() + + let removeSubscription (id: SubscriptionId) (subscriptions : SubscriptionsDict) = + if subscriptions.ContainsKey(id) then + subscriptions.[id] + |> executeOnUnsubscribeAndDispose id + subscriptions.Remove(id) |> ignore + + let removeAllSubscriptions (subscriptions : SubscriptionsDict) = + subscriptions + |> Seq.iter + (fun subscription -> + subscription.Value + |> executeOnUnsubscribeAndDispose subscription.Key + ) + subscriptions.Clear() \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs new file mode 100644 index 000000000..85f3547c4 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs @@ -0,0 +1,376 @@ +namespace FSharp.Data.GraphQL.Server.AppInfrastructure + +open FSharp.Data.GraphQL +open Microsoft.AspNetCore.Http +open FSharp.Data.GraphQL.Server.AppInfrastructure.Rop +open System +open System.Collections.Generic +open System.Net.WebSockets +open System.Text.Json +open System.Threading +open System.Threading.Tasks +open FSharp.Data.GraphQL.Execution +open Microsoft.Extensions.Hosting +open Microsoft.Extensions.Logging + +type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifetime : IHostApplicationLifetime, serviceProvider : IServiceProvider, logger : ILogger>, options : GraphQLOptions<'Root>) = + + let serializeServerMessage (jsonSerializerOptions: JsonSerializerOptions) (serverMessage : ServerMessage) = + task { + let raw = + match serverMessage with + | ConnectionAck -> + { Id = None + Type = "connection_ack" + Payload = None } + | ServerPing -> + { Id = None + Type = "ping" + Payload = None } + | ServerPong p -> + { Id = None + Type = "pong" + Payload = p |> Option.map CustomResponse } + | Next (id, payload) -> + { Id = Some id + Type = "next" + Payload = Some <| ExecutionResult payload } + | Complete id -> + { Id = Some id + Type = "complete" + Payload = None } + | Error (id, errMsgs) -> + { Id = Some id + Type = "error" + Payload = Some <| ErrorMessages errMsgs } + return JsonSerializer.Serialize(raw, jsonSerializerOptions) + } + + let deserializeClientMessage (serializerOptions : JsonSerializerOptions) (msg: string) = + task { + try + return + JsonSerializer.Deserialize(msg, serializerOptions) + |> succeed + with + | :? InvalidMessageException as e -> + return + fail <| InvalidMessage(4400, e.Message.ToString()) + | :? JsonException as e -> + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(e.ToString()) + else + () + return + fail <| InvalidMessage(4400, "invalid json in client message") + } + + let isSocketOpen (theSocket : WebSocket) = + not (theSocket.State = WebSocketState.Aborted) && + not (theSocket.State = WebSocketState.Closed) && + not (theSocket.State = WebSocketState.CloseReceived) + + let canCloseSocket (theSocket : WebSocket) = + not (theSocket.State = WebSocketState.Aborted) && + not (theSocket.State = WebSocketState.Closed) + + let receiveMessageViaSocket (cancellationToken : CancellationToken) (serializerOptions: JsonSerializerOptions) (executor : Executor<'Root>) (socket : WebSocket) : Task option> = + task { + let buffer = Array.zeroCreate 4096 + let completeMessage = new List() + let mutable segmentResponse : WebSocketReceiveResult = null + while (not cancellationToken.IsCancellationRequested) && + socket |> isSocketOpen && + ((segmentResponse = null) || (not segmentResponse.EndOfMessage)) do + try + let! r = socket.ReceiveAsync(new ArraySegment(buffer), cancellationToken) + segmentResponse <- r + completeMessage.AddRange(new ArraySegment(buffer, 0, r.Count)) + with :? OperationCanceledException -> + () + + let message = + completeMessage + |> Seq.filter (fun x -> x > 0uy) + |> Array.ofSeq + |> System.Text.Encoding.UTF8.GetString + if String.IsNullOrWhiteSpace message then + return None + else + let! result = + message + |> deserializeClientMessage serializerOptions + return Some result + } + + let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) = + task { + if not (socket.State = WebSocketState.Open) then + if logger.IsEnabled(LogLevel.Trace) then + logger.LogTrace(sprintf "ignoring message to be sent via socket, since its state is not 'Open', but '%A'" socket.State) + else + let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions + let segment = + new ArraySegment( + System.Text.Encoding.UTF8.GetBytes(serializedMessage) + ) + if not (socket.State = WebSocketState.Open) then + if logger.IsEnabled(LogLevel.Trace) then + logger.LogTrace(sprintf "ignoring message to be sent via socket, since its state is not 'Open', but '%A'" socket.State) + else + do! socket.SendAsync(segment, WebSocketMessageType.Text, endOfMessage = true, cancellationToken = new CancellationToken()) + + if logger.IsEnabled(LogLevel.Trace) then + logger.LogTrace(sprintf "<- %A" message) + } + + let addClientSubscription (id : SubscriptionId) (jsonSerializerOptions) (socket) (howToSendDataOnNext: SubscriptionId -> Output -> Task) (streamSource: IObservable) (subscriptions : SubscriptionsDict) = + let observer = new Reactive.AnonymousObserver( + onNext = + (fun theOutput -> + theOutput + |> howToSendDataOnNext id + |> Async.AwaitTask + |> Async.RunSynchronously + ), + onError = + (fun ex -> + logger.LogError(sprintf "[Error on subscription \"%s\"]: %s" id (ex.ToString())) + ), + onCompleted = + (fun () -> + Complete id + |> sendMessageViaSocket jsonSerializerOptions (socket) + |> Async.AwaitTask + |> Async.RunSynchronously + subscriptions + |> GraphQLSubscriptionsManagement.removeSubscription(id) + ) + ) + + let unsubscriber = streamSource.Subscribe(observer) + + subscriptions + |> GraphQLSubscriptionsManagement.addSubscription(id, unsubscriber, (fun _ -> ())) + + let tryToGracefullyCloseSocket (code, message) theSocket = + task { + if theSocket |> canCloseSocket + then + do! theSocket.CloseAsync(code, message, new CancellationToken()) + else + () + } + + let tryToGracefullyCloseSocketWithDefaultBehavior = + tryToGracefullyCloseSocket (WebSocketCloseStatus.NormalClosure, "Normal Closure") + + let handleMessages (cancellationToken: CancellationToken) (serializerOptions: JsonSerializerOptions) (executor : Executor<'Root>) (root: unit -> 'Root) (pingHandler : PingHandler option) (socket : WebSocket) = + let subscriptions = new Dictionary() + // ----------> + // Helpers --> + // ----------> + let safe_ReceiveMessageViaSocket = receiveMessageViaSocket (new CancellationToken()) + + let safe_Send = sendMessageViaSocket serializerOptions socket + let safe_Receive() = + socket + |> safe_ReceiveMessageViaSocket serializerOptions executor + + let safe_SendQueryOutput id output = + let outputAsDict = output :> IDictionary + match outputAsDict.TryGetValue("errors") with + | true, theValue -> + // The specification says: "This message terminates the operation and no further messages will be sent." + subscriptions + |> GraphQLSubscriptionsManagement.removeSubscription(id) + safe_Send (Error (id, unbox theValue)) + | false, _ -> + safe_Send (Next (id, output)) + + let sendQueryOutputDelayedBy (cancToken: CancellationToken) (ms: int) id output = + task { + do! Async.StartAsTask(Async.Sleep ms, cancellationToken = cancToken) + do! output + |> safe_SendQueryOutput id + } + let safe_SendQueryOutputDelayedBy = sendQueryOutputDelayedBy cancellationToken + + let safe_ApplyPlanExecutionResult (id: SubscriptionId) (socket) (executionResult: GQLResponse) = + task { + match executionResult with + | Stream observableOutput -> + subscriptions + |> addClientSubscription id serializerOptions socket safe_SendQueryOutput observableOutput + | Deferred (data, errors, observableOutput) -> + do! data + |> safe_SendQueryOutput id + if errors.IsEmpty then + subscriptions + |> addClientSubscription id serializerOptions socket (safe_SendQueryOutputDelayedBy 5000) observableOutput + else + () + | Direct (data, _) -> + do! data + |> safe_SendQueryOutput id + } + + let getStrAddendumOfOptionalPayload optionalPayload = + optionalPayload + |> Option.map (fun payloadStr -> sprintf " with payload: %A" payloadStr) + |> Option.defaultWith (fun () -> "") + + let logMsgReceivedWithOptionalPayload optionalPayload msgAsStr = + if logger.IsEnabled(LogLevel.Trace) then + logger.LogTrace (sprintf "%s%s" msgAsStr (optionalPayload |> getStrAddendumOfOptionalPayload)) + + let logMsgWithIdReceived id msgAsStr = + if logger.IsEnabled(LogLevel.Trace) then + logger.LogTrace(sprintf "%s (id: %s)" msgAsStr id) + + // <-------------- + // <-- Helpers --| + // <-------------- + + // -------> + // Main --> + // -------> + task { + try + while not cancellationToken.IsCancellationRequested && socket |> isSocketOpen do + let! receivedMessage = safe_Receive() + match receivedMessage with + | None -> + if logger.IsEnabled(LogLevel.Trace) then + logger.LogTrace(sprintf "Websocket socket received empty message! (socket state = %A)" socket.State) + | Some msg -> + match msg with + | Failure failureMsgs -> + "InvalidMessage" |> logMsgReceivedWithOptionalPayload None + match failureMsgs |> List.head with + | InvalidMessage (code, explanation) -> + do! socket.CloseAsync(enum code, explanation, new CancellationToken()) + | Success (ConnectionInit p, _) -> + "ConnectionInit" |> logMsgReceivedWithOptionalPayload p + do! socket.CloseAsync( + enum CustomWebSocketStatus.tooManyInitializationRequests, + "too many initialization requests", + new CancellationToken()) + | Success (ClientPing p, _) -> + "ClientPing" |> logMsgReceivedWithOptionalPayload p + match pingHandler with + | Some func -> + let! customP = p |> func serviceProvider + do! ServerPong customP |> safe_Send + | None -> + do! ServerPong p |> safe_Send + | Success (ClientPong p, _) -> + "ClientPong" |> logMsgReceivedWithOptionalPayload p + | Success (Subscribe (id, query), _) -> + "Subscribe" |> logMsgWithIdReceived id + if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then + do! socket.CloseAsync( + enum CustomWebSocketStatus.subscriberAlreadyExists, + sprintf "Subscriber for %s already exists" id, + new CancellationToken()) + else + let! planExecutionResult = + executor.AsyncExecute(query.ExecutionPlan, root(), query.Variables) + |> Async.StartAsTask + do! planExecutionResult + |> safe_ApplyPlanExecutionResult id socket + | Success (ClientComplete id, _) -> + "ClientComplete" |> logMsgWithIdReceived id + subscriptions |> GraphQLSubscriptionsManagement.removeSubscription (id) + logger.LogTrace "Leaving graphql-ws connection loop..." + do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior + with + | ex -> + logger.LogError (sprintf "Unexpected exception \"%s\" in GraphQLWebsocketMiddleware (handleMessages). More:\n%s" (ex.GetType().Name) (ex.ToString())) + // at this point, only something really weird must have happened. + // In order to avoid faulty state scenarios and unimagined damages, + // just close the socket without further ado. + do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior + } + + // <-------- + // <-- Main + // <-------- + + let waitForConnectionInitAndRespondToClient (serializerOptions : JsonSerializerOptions) (schemaExecutor : Executor<'Root>) (connectionInitTimeoutInMs : int) (socket : WebSocket) : Task> = + task { + let timerTokenSource = new CancellationTokenSource() + timerTokenSource.CancelAfter(connectionInitTimeoutInMs) + let detonationRegistration = timerTokenSource.Token.Register(fun _ -> + socket + |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.connectionTimeout, "Connection initialisation timeout") + |> Async.AwaitTask + |> Async.RunSynchronously + ) + let! connectionInitSucceeded = Task.Run((fun _ -> + task { + logger.LogDebug("Waiting for ConnectionInit...") + let! receivedMessage = receiveMessageViaSocket (new CancellationToken()) serializerOptions schemaExecutor socket + match receivedMessage with + | Some (Success (ConnectionInit payload, _)) -> + logger.LogDebug("Valid connection_init received! Responding with ACK!") + detonationRegistration.Unregister() |> ignore + do! ConnectionAck |> sendMessageViaSocket serializerOptions socket + return true + | Some (Success (Subscribe _, _)) -> + do! + socket + |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.unauthorized, "Unauthorized") + return false + | Some (Failure [InvalidMessage (code, explanation)]) -> + do! + socket + |> tryToGracefullyCloseSocket (enum code, explanation) + return false + | _ -> + do! + socket + |> tryToGracefullyCloseSocketWithDefaultBehavior + return false + }), timerTokenSource.Token) + if (not timerTokenSource.Token.IsCancellationRequested) then + if connectionInitSucceeded then + return (succeed ()) + else + return (fail "ConnectionInit failed (not because of timeout)") + else + return (fail "ConnectionInit timeout") + } + + member __.InvokeAsync(ctx : HttpContext) = + task { + if not (ctx.Request.Path = PathString (options.WebsocketOptions.EndpointUrl)) then + do! next.Invoke(ctx) + else + if ctx.WebSockets.IsWebSocketRequest then + use! socket = ctx.WebSockets.AcceptWebSocketAsync("graphql-transport-ws") + let! connectionInitResult = + socket + |> waitForConnectionInitAndRespondToClient options.SerializerOptions options.SchemaExecutor options.WebsocketOptions.ConnectionInitTimeoutInMs + match connectionInitResult with + | Failure errMsg -> + logger.LogWarning(sprintf "%A" errMsg) + | Success _ -> + let longRunningCancellationToken = + (CancellationTokenSource.CreateLinkedTokenSource(ctx.RequestAborted, applicationLifetime.ApplicationStopping).Token) + longRunningCancellationToken.Register(fun _ -> + socket + |> tryToGracefullyCloseSocketWithDefaultBehavior + |> Async.AwaitTask + |> Async.RunSynchronously + ) |> ignore + let safe_HandleMessages = handleMessages longRunningCancellationToken + try + do! socket + |> safe_HandleMessages options.SerializerOptions options.SchemaExecutor options.RootFactory options.WebsocketOptions.CustomPingHandler + with + | ex -> + logger.LogError(sprintf "Unexpected exception \"%s\" in GraphQLWebsocketMiddleware. More:\n%s" (ex.GetType().Name) (ex.ToString())) + else + do! next.Invoke(ctx) + } \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Messages.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Messages.fs new file mode 100644 index 000000000..f40c8fcd4 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Messages.fs @@ -0,0 +1,62 @@ +namespace FSharp.Data.GraphQL.Server.AppInfrastructure + +open FSharp.Data.GraphQL.Execution +open FSharp.Data.GraphQL.Types +open System +open System.Text.Json +open System.Collections.Generic + +type SubscriptionId = string +type SubscriptionUnsubscriber = IDisposable +type OnUnsubscribeAction = SubscriptionId -> unit +type SubscriptionsDict = IDictionary + +type GraphQLRequest = + { OperationName : string option + Query : string option + Variables : JsonDocument option + Extensions : string option } + +type RawMessage = + { Id : string option + Type : string + Payload : JsonDocument option } + +type ServerRawPayload = + | ExecutionResult of Output + | ErrorMessages of NameValueLookup list + | CustomResponse of JsonDocument + +type RawServerMessage = + { Id : string option + Type : string + Payload : ServerRawPayload option } + +type GraphQLQuery = + { ExecutionPlan : ExecutionPlan + Variables : Map } + +type ClientMessage = + | ConnectionInit of payload: JsonDocument option + | ClientPing of payload: JsonDocument option + | ClientPong of payload: JsonDocument option + | Subscribe of id: string * query: GraphQLQuery + | ClientComplete of id: string + +type ClientMessageProtocolFailure = + | InvalidMessage of code: int * explanation: string + +type ServerMessage = + | ConnectionAck + | ServerPing + | ServerPong of JsonDocument option + | Next of id : string * payload : Output + | Error of id : string * err : NameValueLookup list + | Complete of id : string + +module CustomWebSocketStatus = + let invalidMessage = 4400 + let unauthorized = 4401 + let connectionTimeout = 4408 + let subscriberAlreadyExists = 4409 + let tooManyInitializationRequests = 4429 \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/README.md b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/README.md new file mode 100644 index 000000000..25f2d753f --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/README.md @@ -0,0 +1,100 @@ +## Usage + +### Server + +In a `Startup` class... +```fsharp +namespace MyApp + +open Giraffe +open FSharp.Data.GraphQL.Server.AppInfrastructure.Giraffe +open FSharp.Data.GraphQL.Server.AppInfrastructure +open Microsoft.AspNetCore.Server.Kestrel.Core +open Microsoft.AspNetCore.Builder +open Microsoft.Extensions.Configuration +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Hosting +open Microsoft.Extensions.Logging +open System +open System.Text.Json + +type Startup private () = + // Factory for object holding request-wide info. You define Root somewhere else. + let rootFactory () : Root = + { RequestId = Guid.NewGuid().ToString() } + + new (configuration: IConfiguration) as this = + Startup() then + this.Configuration <- configuration + + member _.ConfigureServices(services: IServiceCollection) = + services.AddGiraffe() + .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) + .AddGraphQLOptions( // STEP 1: Setting the options + Schema.executor, // --> Schema.executor is defined by you somewhere else (in another file) + rootFactory, + "/ws" // --> endpoint for websocket connections + ) + |> ignore + + member _.Configure(app: IApplicationBuilder, applicationLifetime : IHostApplicationLifetime, loggerFactory : ILoggerFactory) = + let errorHandler (ex : Exception) (log : ILogger) = + log.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.") + clearResponse >=> setStatusCode 500 + app + .UseGiraffeErrorHandler(errorHandler) + .UseWebSockets() + .UseWebSocketsForGraphQL() // STEP 2: using the GraphQL websocket middleware + .UseGiraffe + (HttpHandlers.handleGraphQL + applicationLifetime.ApplicationStopping + (loggerFactory.CreateLogger("HttpHandlers.handlerGraphQL")) + ) + + member val Configuration : IConfiguration = null with get, set + +``` + +In your schema, you'll want to define a subscription, like in (example taken from the star-wars-api sample in the "samples/" folder): + +```fsharp + let Subscription = + Define.SubscriptionObject( + name = "Subscription", + fields = [ + Define.SubscriptionField( + "watchMoon", + RootType, + PlanetType, + "Watches to see if a planet is a moon.", + [ Define.Input("id", String) ], + (fun ctx _ p -> if ctx.Arg("id") = p.Id then Some p else None)) ]) +``` + +Don't forget to notify subscribers about new values: + +```fsharp + let Mutation = + Define.Object( + name = "Mutation", + fields = [ + Define.Field( + "setMoon", + Nullable PlanetType, + "Defines if a planet is actually a moon or not.", + [ Define.Input("id", String); Define.Input("isMoon", Boolean) ], + fun ctx _ -> + getPlanet (ctx.Arg("id")) + |> Option.map (fun x -> + x.SetMoon(Some(ctx.Arg("isMoon"))) |> ignore + schemaConfig.SubscriptionProvider.Publish "watchMoon" x // here you notify the subscribers upon a mutation + x))]) +``` + +Finally run the server (e.g. make it listen at `localhost:8086`). + +There's a demo chat application backend in the `samples/chat-app` folder that showcases the use of `FSharp.Data.GraphQL.Server.AppInfrastructure` in a real-time application scenario, that is: with usage of GraphQL subscriptions (but not only). +The tried and trusted `star-wars-api` also shows how to use subscriptions, but is a more basic example. As a side note, the implementation in `star-wars-api` was used as a starting point for the development of `FSharp.Data.GraphQL.Server.AppInfrastructure`. + +### Client +Using your favorite (or not :)) client library (e.g.: [Apollo Client](https://www.apollographql.com/docs/react/get-started), [Relay](https://relay.dev), [Strawberry Shake](https://chillicream.com/docs/strawberryshake/v13), [elm-graphql](https://github.com/dillonkearns/elm-graphql) ❤️), just point to `localhost:8086/graphql` (as per the example above) and, as long as the client implements the `graphql-transport-ws` subprotocol, subscriptions should work. diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/Rop.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/Rop.fs new file mode 100644 index 000000000..6a3f8900e --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/Rop.fs @@ -0,0 +1,139 @@ +// https://fsharpforfunandprofit.com/rop/ + +module FSharp.Data.GraphQL.Server.AppInfrastructure.Rop + +/// A Result is a success or failure +/// The Success case has a success value, plus a list of messages +/// The Failure case has just a list of messages +type RopResult<'TSuccess, 'TMessage> = + | Success of 'TSuccess * 'TMessage list + | Failure of 'TMessage list + +/// create a Success with no messages +let succeed x = + Success (x,[]) + +/// create a Success with a message +let succeedWithMsg x msg = + Success (x,[msg]) + +/// create a Failure with a message +let fail msg = + Failure [msg] + +/// A function that applies either fSuccess or fFailure +/// depending on the case. +let either fSuccess fFailure = function + | Success (x,msgs) -> fSuccess (x,msgs) + | Failure errors -> fFailure errors + +/// merge messages with a result +let mergeMessages msgs result = + let fSuccess (x,msgs2) = + Success (x, msgs @ msgs2) + let fFailure errs = + Failure (errs @ msgs) + either fSuccess fFailure result + +/// given a function that generates a new RopResult +/// apply it only if the result is on the Success branch +/// merge any existing messages with the new result +let bindR f result = + let fSuccess (x,msgs) = + f x |> mergeMessages msgs + let fFailure errs = + Failure errs + either fSuccess fFailure result + +/// given a function wrapped in a result +/// and a value wrapped in a result +/// apply the function to the value only if both are Success +let applyR f result = + match f,result with + | Success (f,msgs1), Success (x,msgs2) -> + (f x, msgs1@msgs2) |> Success + | Failure errs, Success (_,msgs) + | Success (_,msgs), Failure errs -> + errs @ msgs |> Failure + | Failure errs1, Failure errs2 -> + errs1 @ errs2 |> Failure + +/// infix version of apply +let (<*>) = applyR + +/// given a function that transforms a value +/// apply it only if the result is on the Success branch +let liftR f result = + let f' = f |> succeed + applyR f' result + +/// given two values wrapped in results apply a function to both +let lift2R f result1 result2 = + let f' = liftR f result1 + applyR f' result2 + +/// given three values wrapped in results apply a function to all +let lift3R f result1 result2 result3 = + let f' = lift2R f result1 result2 + applyR f' result3 + +/// given four values wrapped in results apply a function to all +let lift4R f result1 result2 result3 result4 = + let f' = lift3R f result1 result2 result3 + applyR f' result4 + +/// infix version of liftR +let () = liftR + +/// synonym for liftR +let mapR = liftR + +/// given an RopResult, call a unit function on the success branch +/// and pass thru the result +let successTee f result = + let fSuccess (x,msgs) = + f (x,msgs) + Success (x,msgs) + let fFailure errs = Failure errs + either fSuccess fFailure result + +/// given an RopResult, call a unit function on the failure branch +/// and pass thru the result +let failureTee f result = + let fSuccess (x,msgs) = Success (x,msgs) + let fFailure errs = + f errs + Failure errs + either fSuccess fFailure result + +/// given an RopResult, map the messages to a different error type +let mapMessagesR f result = + match result with + | Success (x,msgs) -> + let msgs' = List.map f msgs + Success (x, msgs') + | Failure errors -> + let errors' = List.map f errors + Failure errors' + +/// given an RopResult, in the success case, return the value. +/// In the failure case, determine the value to return by +/// applying a function to the errors in the failure case +let valueOrDefault f result = + match result with + | Success (x,_) -> x + | Failure errors -> f errors + +/// lift an option to a RopResult. +/// Return Success if Some +/// or the given message if None +let failIfNone message = function + | Some x -> succeed x + | None -> fail message + +/// given an RopResult option, return it +/// or the given message if None +let failIfNoneR message = function + | Some rop -> rop + | None -> fail message + diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/RopAsync.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/RopAsync.fs new file mode 100644 index 000000000..ffa84e307 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/RopAsync.fs @@ -0,0 +1,20 @@ +module FSharp.Data.GraphQL.Server.AppInfrastructure.RopAsync + +open Rop +open System.Threading.Tasks + +let bindToAsyncTaskR<'a, 'b, 'c> (f: 'a -> Task>) result = + task { + let secondResult = + match result with + | Success (x, msgs) -> + task { + let! secRes = f x + return secRes |> mergeMessages msgs + } + | Failure errs -> + task { + return Failure errs + } + return! secondResult + } \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/GraphQLQueryDecoding.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/GraphQLQueryDecoding.fs new file mode 100644 index 000000000..65e03f4f2 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/GraphQLQueryDecoding.fs @@ -0,0 +1,76 @@ +namespace FSharp.Data.GraphQL.Server.AppInfrastructure + +module GraphQLQueryDecoding = + open FSharp.Data.GraphQL + open Rop + open System + open System.Text.Json + open System.Text.Json.Serialization + + let private resolveVariables (serializerOptions : JsonSerializerOptions) (expectedVariables : Types.VarDef list) (variableValuesObj : JsonDocument) = + try + try + if (not (variableValuesObj.RootElement.ValueKind.Equals(JsonValueKind.Object))) then + let offendingValueKind = variableValuesObj.RootElement.ValueKind + fail (sprintf "\"variables\" must be an object, but here it is \"%A\" instead" offendingValueKind) + else + let providedVariableValues = variableValuesObj.RootElement.EnumerateObject() |> List.ofSeq + expectedVariables + |> List.choose + (fun expectedVariable -> + providedVariableValues + |> List.tryFind(fun x -> x.Name = expectedVariable.Name) + |> Option.map + (fun providedValue -> + let boxedValue = + if providedValue.Value.ValueKind.Equals(JsonValueKind.Null) then + null :> obj + elif providedValue.Value.ValueKind.Equals(JsonValueKind.String) then + providedValue.Value.GetString() :> obj + else + JsonSerializer.Deserialize(providedValue.Value, serializerOptions) :> obj + (expectedVariable.Name, boxedValue) + ) + ) + |> Map.ofList + |> succeed + with + | :? JsonException as ex -> + fail (sprintf "%s" (ex.Message)) + | :? GraphQLException as ex -> + fail (sprintf "%s" (ex.Message)) + | ex -> + printfn "%s" (ex.ToString()) + fail "Something unexpected happened during the parsing of this request." + finally + variableValuesObj.Dispose() + + let decodeGraphQLQuery (serializerOptions : JsonSerializerOptions) (executor : Executor<'a>) (operationName : string option) (variables : JsonDocument option) (query : string) = + let executionPlanResult = + try + match operationName with + | Some operationName -> + executor.CreateExecutionPlan(query, operationName = operationName) + |> succeed + | None -> + executor.CreateExecutionPlan(query) + |> succeed + with + | :? JsonException as ex -> + fail (sprintf "%s" (ex.Message)) + | :? GraphQLException as ex -> + fail (sprintf "%s" (ex.Message)) + + executionPlanResult + |> bindR + (fun executionPlan -> + match variables with + | None -> succeed <| (executionPlan, Map.empty) // it's none of our business here if some variables are expected. If that's the case, execution of the ExecutionPlan will take care of that later (and issue an error). + | Some variableValuesObj -> + variableValuesObj + |> resolveVariables serializerOptions executionPlan.Variables + |> mapR (fun variableValsObj -> (executionPlan, variableValsObj)) + ) + |> mapR (fun (executionPlan, variables) -> + { ExecutionPlan = executionPlan + Variables = variables }) \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/JsonConverters.fs new file mode 100644 index 000000000..76603ee9c --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/JsonConverters.fs @@ -0,0 +1,161 @@ +namespace FSharp.Data.GraphQL.Server.AppInfrastructure + +open FSharp.Data.GraphQL +open Rop +open System +open System.Text.Json +open System.Text.Json.Serialization + +[] +type ClientMessageConverter<'Root>(executor : Executor<'Root>) = + inherit JsonConverter() + + let raiseInvalidMsg explanation = + raise <| InvalidMessageException explanation + + /// From the spec: "Receiving a message of a type or format which is not specified in this document will result in an immediate socket closure with the event 4400: <error-message>. + /// The <error-message> can be vaguely descriptive on why the received message is invalid." + let invalidMsg (explanation : string) = + InvalidMessage (4400, explanation) + |> fail + + let unpackRopResult ropResult = + match ropResult with + | Success (x, _) -> x + | Failure (failures : ClientMessageProtocolFailure list) -> + System.String.Join("\n\n", (failures |> Seq.map (fun (InvalidMessage (_, explanation)) -> explanation))) + |> raiseInvalidMsg + + let getOptionalString (reader : byref) = + if reader.TokenType.Equals(JsonTokenType.Null) then + None + else + Some (reader.GetString()) + + let readPropertyValueAsAString (propertyName : string) (reader : byref) = + if reader.Read() then + getOptionalString(&reader) + else + raiseInvalidMsg <| sprintf "was expecting a value for property \"%s\"" propertyName + + let requireId (raw : RawMessage) : RopResult = + match raw.Id with + | Some s -> succeed s + | None -> invalidMsg <| "property \"id\" is required for this message but was not present." + + let requireSubscribePayload (serializerOptions : JsonSerializerOptions) (executor : Executor<'a>) (payload : JsonDocument option) : RopResult = + match payload with + | None -> + invalidMsg <| "payload is required for this message, but none was present." + | Some p -> + let rawSubsPayload = JsonSerializer.Deserialize(p, serializerOptions) + match rawSubsPayload with + | None -> + invalidMsg <| "payload is required for this message, but none was present." + | Some subscribePayload -> + match subscribePayload.Query with + | None -> + invalidMsg <| sprintf "there was no query in the client's subscribe message!" + | Some query -> + query + |> GraphQLQueryDecoding.decodeGraphQLQuery serializerOptions executor subscribePayload.OperationName subscribePayload.Variables + |> mapMessagesR (fun errMsg -> InvalidMessage (CustomWebSocketStatus.invalidMessage, errMsg)) + + + let readRawMessage (reader : byref, options: JsonSerializerOptions) : RawMessage = + if not (reader.TokenType.Equals(JsonTokenType.StartObject)) + then raise (new JsonException((sprintf "reader's first token was not \"%A\", but \"%A\"" JsonTokenType.StartObject reader.TokenType))) + else + let mutable id : string option = None + let mutable theType : string option = None + let mutable payload : JsonDocument option = None + while reader.Read() && (not (reader.TokenType.Equals(JsonTokenType.EndObject))) do + match reader.GetString() with + | "id" -> + id <- readPropertyValueAsAString "id" &reader + | "type" -> + theType <- readPropertyValueAsAString "type" &reader + | "payload" -> + payload <- Some <| JsonDocument.ParseValue(&reader) + | other -> + raiseInvalidMsg <| sprintf "unknown property \"%s\"" other + + match theType with + | None -> + raiseInvalidMsg "property \"type\" is missing" + | Some msgType -> + { Id = id + Type = msgType + Payload = payload } + + override __.Read(reader : byref, typeToConvert: Type, options: JsonSerializerOptions) : ClientMessage = + let raw = readRawMessage(&reader, options) + match raw.Type with + | "connection_init" -> + ConnectionInit raw.Payload + | "ping" -> + ClientPing raw.Payload + | "pong" -> + ClientPong raw.Payload + | "complete" -> + raw + |> requireId + |> mapR ClientComplete + |> unpackRopResult + | "subscribe" -> + raw + |> requireId + |> bindR + (fun id -> + raw.Payload + |> requireSubscribePayload options executor + |> mapR (fun payload -> (id, payload)) + ) + |> mapR Subscribe + |> unpackRopResult + | other -> + raiseInvalidMsg <| sprintf "invalid type \"%s\" specified by client." other + + + + override __.Write(writer : Utf8JsonWriter, value : ClientMessage, options : JsonSerializerOptions) = + failwith "serializing a WebSocketClientMessage is not supported (yet(?))" + +[] +type RawServerMessageConverter() = + inherit JsonConverter() + + override __.Read(reader : byref, typeToConvert: Type, options : JsonSerializerOptions) : RawServerMessage = + failwith "deserializing a RawServerMessage is not supported (yet(?))" + + override __.Write(writer : Utf8JsonWriter, value : RawServerMessage, options : JsonSerializerOptions) = + writer.WriteStartObject() + writer.WriteString("type", value.Type) + match value.Id with + | None -> + () + | Some id -> + writer.WriteString("id", id) + + match value.Payload with + | None -> + () + | Some serverRawPayload -> + match serverRawPayload with + | ExecutionResult output -> + writer.WritePropertyName("payload") + JsonSerializer.Serialize(writer, output, options) + | ErrorMessages msgs -> + JsonSerializer.Serialize(writer, msgs, options) + | CustomResponse jsonDocument -> + jsonDocument.WriteTo(writer) + + writer.WriteEndObject() + + +module JsonConverterUtils = + let configureSerializer (executor : Executor<'Root>) (jsonSerializerOptions : JsonSerializerOptions) = + jsonSerializerOptions.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase + jsonSerializerOptions.Converters.Add(new ClientMessageConverter<'Root>(executor)) + jsonSerializerOptions.Converters.Add(new RawServerMessageConverter()) + jsonSerializerOptions \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/StartupExtensions.fs new file mode 100644 index 000000000..798d97a5f --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/StartupExtensions.fs @@ -0,0 +1,40 @@ +namespace FSharp.Data.GraphQL.Server.AppInfrastructure + +open FSharp.Data.GraphQL +open Microsoft.AspNetCore.Builder +open Microsoft.Extensions.DependencyInjection +open System.Runtime.CompilerServices +open System.Text.Json + +[] +type ServiceCollectionExtensions() = + + static let createStandardOptions executor rootFactory endpointUrl = + { SchemaExecutor = executor + RootFactory = rootFactory + SerializerOptions = + JsonSerializerOptions() + |> JsonConverterUtils.configureSerializer executor + WebsocketOptions = + { EndpointUrl = endpointUrl + ConnectionInitTimeoutInMs = 3000 + CustomPingHandler = None } + } + + [] + static member AddGraphQLOptions<'Root>(this : IServiceCollection, executor : Executor<'Root>, rootFactory : unit -> 'Root, endpointUrl : string) = + this.AddSingleton>(createStandardOptions executor rootFactory endpointUrl) + + [] + static member AddGraphQLOptionsWith<'Root> + ( this : IServiceCollection, + executor : Executor<'Root>, + rootFactory : unit -> 'Root, + endpointUrl : string, + extraConfiguration : GraphQLOptions<'Root> -> GraphQLOptions<'Root> + ) = + this.AddSingleton>(createStandardOptions executor rootFactory endpointUrl |> extraConfiguration) + + [] + static member UseWebSocketsForGraphQL<'Root>(this : IApplicationBuilder) = + this.UseMiddleware>() \ No newline at end of file diff --git a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs b/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs new file mode 100644 index 000000000..acc7edb9d --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs @@ -0,0 +1,147 @@ +module FSharp.Data.GraphQL.Tests.AppInfrastructure.InvalidMessageTests + +open FSharp.Data.GraphQL.Tests.AppInfrastructure +open FSharp.Data.GraphQL.Server.AppInfrastructure +open System.Text.Json +open Xunit + +let toClientMessage (theInput : string) = + let serializerOptions = new JsonSerializerOptions() + serializerOptions.PropertyNameCaseInsensitive <- true + serializerOptions.Converters.Add(new ClientMessageConverter(TestSchema.executor)) + serializerOptions.Converters.Add(new RawServerMessageConverter()) + JsonSerializer.Deserialize(theInput, serializerOptions) + +let willResultInInvalidMessage expectedExplanation input = + try + let result = + input + |> toClientMessage + Assert.Fail(sprintf "should have failed, but succeeded with result: '%A'" result) + with + | :? JsonException as ex -> + Assert.Equal(expectedExplanation, ex.Message) + | :? InvalidMessageException as ex -> + Assert.Equal(expectedExplanation, ex.Message) + +let willResultInJsonException input = + try + input + |> toClientMessage + |> ignore + Assert.Fail("expected that a JsonException would have already been thrown at this point") + with + | :? JsonException as ex -> + Assert.True(true) + +[] +let ``unknown message type`` () = + """{ + "type": "connection_start" + } + """ + |> willResultInInvalidMessage "invalid type \"connection_start\" specified by client." + +[] +let ``type not specified`` () = + """{ + "payload": "hello, let us connect" + } + """ + |> willResultInInvalidMessage "property \"type\" is missing" + +[] +let ``no payload in subscribe message`` () = + """{ + "type": "subscribe", + "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6" + } + """ + |> willResultInInvalidMessage "payload is required for this message, but none was present." + +[] +let ``null payload json in subscribe message`` () = + """{ + "type": "subscribe", + "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", + "payload": null + } + """ + |> willResultInInvalidMessage "payload is required for this message, but none was present." + +[] +let ``payload type of number in subscribe message`` () = + """{ + "type": "subscribe", + "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", + "payload": 42 + } + """ + |> willResultInInvalidMessage "The JSON value could not be converted to GraphQLTransportWS.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 2." + +[] +let ``no id in subscribe message`` () = + """{ + "type": "subscribe", + "payload": { + "query": "subscription { watchMoon(id: \"1\") { id name isMoon } }" + } + } + """ + |> willResultInInvalidMessage "property \"id\" is required for this message but was not present." + +[] +let ``string payload wrongly used in subscribe`` () = + """{ + "type": "subscribe", + "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", + "payload": "{\"query\": \"subscription { watchMoon(id: \\\"1\\\") { id name isMoon } }\"}" + } + """ + |> willResultInInvalidMessage "The JSON value could not be converted to GraphQLTransportWS.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 79." + +[] +let ``id is incorrectly a number in a subscribe message`` () = + """{ + "type": "subscribe", + "id": 42, + "payload": { + "query": "subscription { watchMoon(id: \"1\") { id name isMoon } }" + } + } + """ + |> willResultInJsonException + +[] +let ``typo in one of the messages root properties`` () = + """{ + "typo": "subscribe", + "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", + "payload": { + "query": "subscription { watchMoon(id: \"1\") { id name isMoon } }" + } + } + """ + |> willResultInInvalidMessage "unknown property \"typo\"" + +[] +let ``complete message without an id`` () = + """{ + "type": "complete" + } + """ + |> willResultInInvalidMessage "property \"id\" is required for this message but was not present." + +[] +let ``complete message with a null id`` () = + """{ + "type": "complete", + "id": null + } + """ + |> willResultInInvalidMessage "property \"id\" is required for this message but was not present." + + + + + diff --git a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/SerializationTests.fs b/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/SerializationTests.fs new file mode 100644 index 000000000..aaac657a5 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/SerializationTests.fs @@ -0,0 +1,157 @@ +module FSharp.Data.GraphQL.Tests.AppInfrastructure.SerializationTests + +open FSharp.Data.GraphQL.Ast +open FSharp.Data.GraphQL.Server.AppInfrastructure +open System.Text.Json +open Xunit + +let getStdSerializerOptions () = + let serializerOptions = new JsonSerializerOptions() + serializerOptions.PropertyNameCaseInsensitive <- true + serializerOptions.Converters.Add(new ClientMessageConverter(TestSchema.executor)) + serializerOptions.Converters.Add(new RawServerMessageConverter()) + serializerOptions + +[] +let ``Deserializes ConnectionInit correctly`` () = + let serializerOptions = getStdSerializerOptions() + let input = "{\"type\":\"connection_init\"}" + + let result = JsonSerializer.Deserialize(input, serializerOptions) + + match result with + | ConnectionInit None -> () // <-- expected + | other -> + Assert.Fail(sprintf "unexpected actual value: '%A'" other) + +[] +let ``Deserializes ConnectionInit with payload correctly`` () = + let serializerOptions = getStdSerializerOptions() + + let input = "{\"type\":\"connection_init\", \"payload\":\"hello\"}" + + let result = JsonSerializer.Deserialize(input, serializerOptions) + + match result with + | ConnectionInit _ -> () // <-- expected + | other -> + Assert.Fail(sprintf "unexpected actual value: '%A'" other) + +[] +let ``Deserializes ClientPing correctly`` () = + let serializerOptions = getStdSerializerOptions() + + let input = "{\"type\":\"ping\"}" + + let result = JsonSerializer.Deserialize(input, serializerOptions) + + match result with + | ClientPing None -> () // <-- expected + | other -> + Assert.Fail(sprintf "unexpected actual value '%A'" other) + +[] +let ``Deserializes ClientPing with payload correctly`` () = + let serializerOptions = getStdSerializerOptions() + + let input = "{\"type\":\"ping\", \"payload\":\"ping!\"}" + + let result = JsonSerializer.Deserialize(input, serializerOptions) + + match result with + | ClientPing _ -> () // <-- expected + | other -> + Assert.Fail(sprintf "unexpected actual value '%A" other) + +[] +let ``Deserializes ClientPong correctly`` () = + let serializerOptions = getStdSerializerOptions() + + let input = "{\"type\":\"pong\"}" + + let result = JsonSerializer.Deserialize(input, serializerOptions) + + match result with + | ClientPong None -> () // <-- expected + | other -> + Assert.Fail(sprintf "unexpected actual value: '%A'" other) + +[] +let ``Deserializes ClientPong with payload correctly`` () = + let serializerOptions = getStdSerializerOptions() + + let input = "{\"type\":\"pong\", \"payload\": \"pong!\"}" + + let result = JsonSerializer.Deserialize(input, serializerOptions) + + match result with + | ClientPong _ -> () // <-- expected + | other -> + Assert.Fail(sprintf "unexpected actual value: '%A'" other) + +[] +let ``Deserializes ClientComplete correctly``() = + let serializerOptions = getStdSerializerOptions() + + let input = "{\"id\": \"65fca2b5-f149-4a70-a055-5123dea4628f\", \"type\":\"complete\"}" + + let result = JsonSerializer.Deserialize(input, serializerOptions) + + match result with + | ClientComplete id -> + Assert.Equal("65fca2b5-f149-4a70-a055-5123dea4628f", id) + | other -> + Assert.Fail(sprintf "unexpected actual value: '%A'" other) + +[] +let ``Deserializes client subscription correctly`` () = + let serializerOptions = getStdSerializerOptions() + + let input = + """{ + "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", + "type": "subscribe", + "payload" : { + "query": "subscription { watchMoon(id: \"1\") { id name isMoon } }" + } + } + """ + + let result = JsonSerializer.Deserialize(input, serializerOptions) + + match result with + | Subscribe (id, payload) -> + Assert.Equal("b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", id) + Assert.Equal(1, payload.ExecutionPlan.Operation.SelectionSet.Length) + let watchMoonSelection = payload.ExecutionPlan.Operation.SelectionSet |> List.head + match watchMoonSelection with + | Field watchMoonField -> + Assert.Equal("watchMoon", watchMoonField.Name) + Assert.Equal(1, watchMoonField.Arguments.Length) + let watchMoonFieldArg = watchMoonField.Arguments |> List.head + Assert.Equal("id", watchMoonFieldArg.Name) + match watchMoonFieldArg.Value with + | StringValue theValue -> + Assert.Equal("1", theValue) + | other -> + Assert.Fail(sprintf "expected arg to be a StringValue, but it was: %A" other) + Assert.Equal(3, watchMoonField.SelectionSet.Length) + match watchMoonField.SelectionSet.[0] with + | Field firstField -> + Assert.Equal("id", firstField.Name) + | other -> + Assert.Fail(sprintf "expected field to be a Field, but it was: %A" other) + match watchMoonField.SelectionSet.[1] with + | Field secondField -> + Assert.Equal("name", secondField.Name) + | other -> + Assert.Fail(sprintf "expected field to be a Field, but it was: %A" other) + match watchMoonField.SelectionSet.[2] with + | Field thirdField -> + Assert.Equal("isMoon", thirdField.Name) + | other -> + Assert.Fail(sprintf "expected field to be a Field, but it was: %A" other) + | somethingElse -> + Assert.Fail(sprintf "expected it to be a field, but it was: %A" somethingElse) + | other -> + Assert.Fail(sprintf "unexpected actual value: '%A" other) \ No newline at end of file diff --git a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/TestSchema.fs b/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/TestSchema.fs new file mode 100644 index 000000000..78149f649 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/TestSchema.fs @@ -0,0 +1,229 @@ +namespace FSharp.Data.GraphQL.Tests.AppInfrastructure + +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Types + +#nowarn "40" + +type Episode = + | NewHope = 1 + | Empire = 2 + | Jedi = 3 + +type Human = + { Id : string + Name : string option + Friends : string list + AppearsIn : Episode list + HomePlanet : string option } + +type Droid = + { Id : string + Name : string option + Friends : string list + AppearsIn : Episode list + PrimaryFunction : string option } + +type Planet = + { Id : string + Name : string option + mutable IsMoon : bool option } + member x.SetMoon b = + x.IsMoon <- b + x + +type Root = + { RequestId: string } + +type Character = + | Human of Human + | Droid of Droid + +module TestSchema = + let humans = + [ { Id = "1000" + Name = Some "Luke Skywalker" + Friends = [ "1002"; "1003"; "2000"; "2001" ] + AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] + HomePlanet = Some "Tatooine" } + { Id = "1001" + Name = Some "Darth Vader" + Friends = [ "1004" ] + AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] + HomePlanet = Some "Tatooine" } + { Id = "1002" + Name = Some "Han Solo" + Friends = [ "1000"; "1003"; "2001" ] + AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] + HomePlanet = None } + { Id = "1003" + Name = Some "Leia Organa" + Friends = [ "1000"; "1002"; "2000"; "2001" ] + AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] + HomePlanet = Some "Alderaan" } + { Id = "1004" + Name = Some "Wilhuff Tarkin" + Friends = [ "1001" ] + AppearsIn = [ Episode.NewHope ] + HomePlanet = None } ] + + let droids = + [ { Id = "2000" + Name = Some "C-3PO" + Friends = [ "1000"; "1002"; "1003"; "2001" ] + AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] + PrimaryFunction = Some "Protocol" } + { Id = "2001" + Name = Some "R2-D2" + Friends = [ "1000"; "1002"; "1003" ] + AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] + PrimaryFunction = Some "Astromech" } ] + + let planets = + [ { Id = "1" + Name = Some "Tatooine" + IsMoon = Some false} + { Id = "2" + Name = Some "Endor" + IsMoon = Some true} + { Id = "3" + Name = Some "Death Star" + IsMoon = Some false}] + + let getHuman id = + humans |> List.tryFind (fun h -> h.Id = id) + + let getDroid id = + droids |> List.tryFind (fun d -> d.Id = id) + + let getPlanet id = + planets |> List.tryFind (fun p -> p.Id = id) + + let characters = + (humans |> List.map Human) @ (droids |> List.map Droid) + + let matchesId id = function + | Human h -> h.Id = id + | Droid d -> d.Id = id + + let getCharacter id = + characters |> List.tryFind (matchesId id) + + let EpisodeType = + Define.Enum( + name = "Episode", + description = "One of the films in the Star Wars Trilogy.", + options = [ + Define.EnumValue("NewHope", Episode.NewHope, "Released in 1977.") + Define.EnumValue("Empire", Episode.Empire, "Released in 1980.") + Define.EnumValue("Jedi", Episode.Jedi, "Released in 1983.") ]) + + let rec CharacterType = + Define.Union( + name = "Character", + description = "A character in the Star Wars Trilogy.", + options = [ HumanType; DroidType ], + resolveValue = (fun o -> + match o with + | Human h -> box h + | Droid d -> upcast d), + resolveType = (fun o -> + match o with + | Human _ -> upcast HumanType + | Droid _ -> upcast DroidType)) + + and HumanType : ObjectDef = + Define.Object( + name = "Human", + description = "A humanoid creature in the Star Wars universe.", + isTypeOf = (fun o -> o :? Human), + fieldsFn = fun () -> + [ + Define.Field("id", String, "The id of the human.", fun _ (h : Human) -> h.Id) + Define.Field("name", Nullable String, "The name of the human.", fun _ (h : Human) -> h.Name) + Define.Field("friends", ListOf (Nullable CharacterType), "The friends of the human, or an empty list if they have none.", + fun _ (h : Human) -> h.Friends |> List.map getCharacter |> List.toSeq) + Define.Field("appearsIn", ListOf EpisodeType, "Which movies they appear in.", fun _ (h : Human) -> h.AppearsIn) + Define.Field("homePlanet", Nullable String, "The home planet of the human, or null if unknown.", fun _ h -> h.HomePlanet) + ]) + + and DroidType = + Define.Object( + name = "Droid", + description = "A mechanical creature in the Star Wars universe.", + isTypeOf = (fun o -> o :? Droid), + fieldsFn = fun () -> + [ + Define.Field("id", String, "The id of the droid.", fun _ (d : Droid) -> d.Id) + Define.Field("name", Nullable String, "The name of the Droid.", fun _ (d : Droid) -> d.Name) + Define.Field("friends", ListOf (Nullable CharacterType), "The friends of the Droid, or an empty list if they have none.", + fun _ (d : Droid) -> d.Friends |> List.map getCharacter |> List.toSeq) + Define.Field("appearsIn", ListOf EpisodeType, "Which movies they appear in.", fun _ d -> d.AppearsIn) + Define.Field("primaryFunction", Nullable String, "The primary function of the droid.", fun _ d -> d.PrimaryFunction) + ]) + + and PlanetType = + Define.Object( + name = "Planet", + description = "A planet in the Star Wars universe.", + isTypeOf = (fun o -> o :? Planet), + fieldsFn = fun () -> + [ + Define.Field("id", String, "The id of the planet", fun _ p -> p.Id) + Define.Field("name", Nullable String, "The name of the planet.", fun _ p -> p.Name) + Define.Field("isMoon", Nullable Boolean, "Is that a moon?", fun _ p -> p.IsMoon) + ]) + + and RootType = + Define.Object( + name = "Root", + description = "The Root type to be passed to all our resolvers.", + isTypeOf = (fun o -> o :? Root), + fieldsFn = fun () -> + [ + Define.Field("requestId", String, "The ID of the client.", fun _ (r : Root) -> r.RequestId) + ]) + + let Query = + Define.Object( + name = "Query", + fields = [ + Define.Field("hero", Nullable HumanType, "Gets human hero", [ Define.Input("id", String) ], fun ctx _ -> getHuman (ctx.Arg("id"))) + Define.Field("droid", Nullable DroidType, "Gets droid", [ Define.Input("id", String) ], fun ctx _ -> getDroid (ctx.Arg("id"))) + Define.Field("planet", Nullable PlanetType, "Gets planet", [ Define.Input("id", String) ], fun ctx _ -> getPlanet (ctx.Arg("id"))) + Define.Field("characters", ListOf CharacterType, "Gets characters", fun _ _ -> characters) ]) + + let Subscription = + Define.SubscriptionObject( + name = "Subscription", + fields = [ + Define.SubscriptionField( + "watchMoon", + RootType, + PlanetType, + "Watches to see if a planet is a moon.", + [ Define.Input("id", String) ], + (fun ctx _ p -> if ctx.Arg("id") = p.Id then Some p else None)) ]) + + let schemaConfig = SchemaConfig.Default + + let Mutation = + Define.Object( + name = "Mutation", + fields = [ + Define.Field( + "setMoon", + Nullable PlanetType, + "Defines if a planet is actually a moon or not.", + [ Define.Input("id", String); Define.Input("isMoon", Boolean) ], + fun ctx _ -> + getPlanet (ctx.Arg("id")) + |> Option.map (fun x -> + x.SetMoon(Some(ctx.Arg("isMoon"))) |> ignore + schemaConfig.SubscriptionProvider.Publish "watchMoon" x + schemaConfig.LiveFieldSubscriptionProvider.Publish "Planet" "isMoon" x + x))]) + + let schema : ISchema = upcast Schema(Query, Mutation, Subscription, schemaConfig) + + let executor = Executor(schema, []) \ No newline at end of file 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 307f51215..012de8e57 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -54,6 +54,9 @@ + + + @@ -66,6 +69,7 @@ + \ No newline at end of file From 83a91983b4c4846cac7606e057645108f7f373d2 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 5 Mar 2023 17:02:43 +0100 Subject: [PATCH 003/100] moving Server.AppInfrastructure near to other projs in sln --- FSharp.Data.GraphQL.sln | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FSharp.Data.GraphQL.sln b/FSharp.Data.GraphQL.sln index f0d1ea728..ca306c6b3 100644 --- a/FSharp.Data.GraphQL.sln +++ b/FSharp.Data.GraphQL.sln @@ -45,6 +45,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Shared" EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Server", "src\FSharp.Data.GraphQL.Server\FSharp.Data.GraphQL.Server.fsproj", "{474179D3-0090-49E9-88F8-2971C0966077}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.GraphQL.Server.AppInfrastructure", "src\FSharp.Data.GraphQL.Server.AppInfrastructure\FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj", "{554A6833-1E72-41B4-AAC1-C19371EC061B}" +EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Server.Relay", "src\FSharp.Data.GraphQL.Server.Relay\FSharp.Data.GraphQL.Server.Relay.fsproj", "{E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}" EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Server.Middleware", "src\FSharp.Data.GraphQL.Server.Middleware\FSharp.Data.GraphQL.Server.Middleware.fsproj", "{8FB23F61-77CB-42C7-8EEC-B22D7C4E4067}" @@ -171,8 +173,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "components", "components", user.jsx = user.jsx EndProjectSection EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.GraphQL.Server.AppInfrastructure", "src\FSharp.Data.GraphQL.Server.AppInfrastructure\FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj", "{554A6833-1E72-41B4-AAC1-C19371EC061B}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 9e88ac96ae4e2581693e88185b86842538102c50 Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sun, 5 Mar 2023 22:30:42 +0100 Subject: [PATCH 004/100] Apply suggestions from code review Co-authored-by: Andrii Chebukin --- samples/chat-app/server/Program.fs | 1 + .../Giraffe/HttpHandlers.fs | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/samples/chat-app/server/Program.fs b/samples/chat-app/server/Program.fs index 86b9c8d3f..9a2e2a991 100644 --- a/samples/chat-app/server/Program.fs +++ b/samples/chat-app/server/Program.fs @@ -11,6 +11,7 @@ open Microsoft.Extensions.Hosting open Microsoft.Extensions.Logging module Program = + let rootFactory () : Root = { RequestId = Guid.NewGuid().ToString() } diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs index e0b13c03c..d50afc2cd 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs @@ -17,12 +17,7 @@ type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult module HttpHandlers = let private getRequiredService<'Service> (ctx : HttpContext) : 'Service = - let service = (ctx.GetService<'Service>()) - if obj.ReferenceEquals(service, null) - then - let theType = typedefof<'Service> - failwithf "required service \"%s\" was not registered at the dependency injection container!" (theType.FullName) - service + ctx.RequestServices.GetrequiredService<'Service>() let private httpOk (cancellationToken : CancellationToken) (serializerOptions : JsonSerializerOptions) payload : HttpHandler = setStatusCode 200 From d3224395ca816b6a2898081dd06ceb342aa9123e Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 5 Mar 2023 22:53:48 +0100 Subject: [PATCH 005/100] correctly calling "GetRequiredService" --- .../Giraffe/HttpHandlers.fs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs index d50afc2cd..97e861062 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs @@ -6,6 +6,7 @@ open Giraffe open FSharp.Data.GraphQL.Server.AppInfrastructure open FSharp.Data.GraphQL.Server.AppInfrastructure.Rop open Microsoft.AspNetCore.Http +open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging open System.Collections.Generic open System.Text.Json @@ -16,9 +17,6 @@ type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult module HttpHandlers = - let private getRequiredService<'Service> (ctx : HttpContext) : 'Service = - ctx.RequestServices.GetrequiredService<'Service>() - let private httpOk (cancellationToken : CancellationToken) (serializerOptions : JsonSerializerOptions) payload : HttpHandler = setStatusCode 200 >=> (setHttpHeader "Content-Type" "application/json") @@ -80,7 +78,7 @@ module HttpHandlers = if cancellationToken.IsCancellationRequested then return (fun _ -> None) ctx else - let options = ctx |> getRequiredService> + let options = ctx.RequestServices.GetRequiredService>() let executor = options.SchemaExecutor let rootFactory = options.RootFactory let serializerOptions = options.SerializerOptions From bf6bd6bf9ea263549c8c80235cb280b803a7117f Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 5 Mar 2023 23:27:32 +0100 Subject: [PATCH 006/100] using standardized namespace for chat-app sample --- samples/chat-app/server/DomainModel.fs | 2 +- samples/chat-app/server/Program.fs | 2 +- samples/chat-app/server/Schema.fs | 2 +- samples/chat-app/server/chat-app.fsproj | 2 +- .../AppInfrastructure/InvalidMessageTests.fs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/chat-app/server/DomainModel.fs b/samples/chat-app/server/DomainModel.fs index 5aa02e6a3..6a02027d5 100644 --- a/samples/chat-app/server/DomainModel.fs +++ b/samples/chat-app/server/DomainModel.fs @@ -1,4 +1,4 @@ -namespace chat_app +namespace FSharp.Data.GraphQL.Samples.ChatApp open System diff --git a/samples/chat-app/server/Program.fs b/samples/chat-app/server/Program.fs index 9a2e2a991..c78db94a8 100644 --- a/samples/chat-app/server/Program.fs +++ b/samples/chat-app/server/Program.fs @@ -1,4 +1,4 @@ -namespace chat_app +namespace FSharp.Data.GraphQL.Samples.ChatApp open Giraffe open FSharp.Data.GraphQL.Server.AppInfrastructure diff --git a/samples/chat-app/server/Schema.fs b/samples/chat-app/server/Schema.fs index b2a17baf8..13e5b609b 100644 --- a/samples/chat-app/server/Schema.fs +++ b/samples/chat-app/server/Schema.fs @@ -1,4 +1,4 @@ -namespace chat_app +namespace FSharp.Data.GraphQL.Samples.ChatApp open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types diff --git a/samples/chat-app/server/chat-app.fsproj b/samples/chat-app/server/chat-app.fsproj index de8125856..ee414838f 100644 --- a/samples/chat-app/server/chat-app.fsproj +++ b/samples/chat-app/server/chat-app.fsproj @@ -2,7 +2,7 @@ net6.0 - chat_app + FSharp.Data.GraphQL.Samples.ChatApp diff --git a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs b/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs index acc7edb9d..b7e20a06a 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs @@ -77,7 +77,7 @@ let ``payload type of number in subscribe message`` () = "payload": 42 } """ - |> willResultInInvalidMessage "The JSON value could not be converted to GraphQLTransportWS.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 2." + |> willResultInInvalidMessage "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AppInfrastructure.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 2." [] let ``no id in subscribe message`` () = @@ -98,7 +98,7 @@ let ``string payload wrongly used in subscribe`` () = "payload": "{\"query\": \"subscription { watchMoon(id: \\\"1\\\") { id name isMoon } }\"}" } """ - |> willResultInInvalidMessage "The JSON value could not be converted to GraphQLTransportWS.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 79." + |> willResultInInvalidMessage "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AppInfrastructure.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 79." [] let ``id is incorrectly a number in a subscribe message`` () = From c63ef2d6782c3b64360950d956c30259f2307f4c Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 5 Mar 2023 23:28:59 +0100 Subject: [PATCH 007/100] renamed sample chat-app to standardized proj name --- ...chat-app.fsproj => FSharp.Data.GraphQL.Samples.ChatApp.fsproj} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename samples/chat-app/server/{chat-app.fsproj => FSharp.Data.GraphQL.Samples.ChatApp.fsproj} (100%) diff --git a/samples/chat-app/server/chat-app.fsproj b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj similarity index 100% rename from samples/chat-app/server/chat-app.fsproj rename to samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj From 639f1e24d5b2ff33430636abe632f15c5ad220f1 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 5 Mar 2023 23:29:25 +0100 Subject: [PATCH 008/100] fixing build (related to https://github.com/dotnet/fsharp/issues/12839 ) --- .../Giraffe/HttpHandlers.fs | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs index 97e861062..446bbe62b 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs @@ -104,6 +104,47 @@ module HttpHandlers = prepareGenericErrors ["subscriptions are not supported here (use the websocket endpoint instead)."] return! httpOk cancellationToken serializerOptions error next ctx } + + let handleDeserializedGraphQLRequest (graphqlRequest : GraphQLRequest) = + task { + match graphqlRequest.Query with + | None -> + let! result = executor.AsyncExecute (Introspection.IntrospectionQuery) |> Async.StartAsTask + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) + else + () + return! result |> applyPlanExecutionResult + | Some queryAsStr -> + let graphQLQueryDecodingResult = + queryAsStr + |> GraphQLQueryDecoding.decodeGraphQLQuery + serializerOptions + executor + graphqlRequest.OperationName + graphqlRequest.Variables + match graphQLQueryDecodingResult with + | Failure errMsgs -> + return! + httpOk cancellationToken serializerOptions (prepareGenericErrors errMsgs) next ctx + | Success (query, _) -> + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(sprintf "Received query: %A" query) + else + () + let root = rootFactory() + let! result = + executor.AsyncExecute( + query.ExecutionPlan, + data = root, + variables = query.Variables + )|> Async.StartAsTask + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) + else + () + return! result |> applyPlanExecutionResult + } if ctx.Request.Headers.ContentLength.GetValueOrDefault(0) = 0 then let! result = executor.AsyncExecute (Introspection.IntrospectionQuery) |> Async.StartAsTask if logger.IsEnabled(LogLevel.Debug) then @@ -116,41 +157,5 @@ module HttpHandlers = | Failure errMsgs -> return! httpOk cancellationToken serializerOptions (prepareGenericErrors errMsgs) next ctx | Success (graphqlRequest, _) -> - match graphqlRequest.Query with - | None -> - let! result = executor.AsyncExecute (Introspection.IntrospectionQuery) |> Async.StartAsTask - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) - else - () - return! result |> applyPlanExecutionResult - | Some queryAsStr -> - let graphQLQueryDecodingResult = - queryAsStr - |> GraphQLQueryDecoding.decodeGraphQLQuery - serializerOptions - executor - graphqlRequest.OperationName - graphqlRequest.Variables - match graphQLQueryDecodingResult with - | Failure errMsgs -> - return! - httpOk cancellationToken serializerOptions (prepareGenericErrors errMsgs) next ctx - | Success (query, _) -> - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(sprintf "Received query: %A" query) - else - () - let root = rootFactory() - let! result = - executor.AsyncExecute( - query.ExecutionPlan, - data = root, - variables = query.Variables - )|> Async.StartAsTask - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) - else - () - return! result |> applyPlanExecutionResult + return! handleDeserializedGraphQLRequest graphqlRequest } \ No newline at end of file From 925873fbc00db5ca043422d5610ddb85774dfe38 Mon Sep 17 00:00:00 2001 From: valber Date: Sat, 11 Mar 2023 19:59:08 +0100 Subject: [PATCH 009/100] fixing HttpHandler: the "errors" property shouldn't be there if no errs I know this code will be removed later because of upcoming changes from another branch, but it's breaking the integration tests (more specifically: the type provider). It just feels better to have it work correctly for the moment (and have a passing build :) ). --- .../Giraffe/HttpHandlers.fs | 30 +++++++++++-------- .../introspection.json | 4 +-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs index 446bbe62b..ab0395b27 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs @@ -55,19 +55,23 @@ module HttpHandlers = data |> Seq.map(fun x -> (x.Key, x.Value)) |> Map.ofSeq - match data.TryGetValue("errors") with - | (true, (:? list as nameValueLookups)) -> - result - |> Map.change - "errors" - (fun _ -> Some <| upcast (nameValueLookups @ (theseNew |> toNameValueLookupList) |> List.distinct)) - | (true, _) -> - result - | (false, _) -> - result - |> Map.add - "errors" - (upcast (theseNew |> toNameValueLookupList)) + + if theseNew |> List.isEmpty then + result // because we want to avoid having an "errors" property as an empty list (according to specification) + else + match data.TryGetValue("errors") with + | (true, (:? list as nameValueLookups)) -> + result + |> Map.change + "errors" + (fun _ -> Some <| upcast (nameValueLookups @ (theseNew |> toNameValueLookupList) |> List.distinct)) + | (true, _) -> + result + | (false, _) -> + result + |> Map.add + "errors" + (upcast (theseNew |> toNameValueLookupList)) let handleGraphQL<'Root> (cancellationToken : CancellationToken) diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json index 64dbbc2b6..1f6c08ee3 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json @@ -1,5 +1,4 @@ { - "documentId": 297716339, "data": { "__schema": { "queryType": { @@ -1739,5 +1738,6 @@ } ] } - } + }, + "documentId": 251746993 } \ No newline at end of file From 6fcbf71fefe907e1110c3df48968022761a24a81 Mon Sep 17 00:00:00 2001 From: valber Date: Sat, 11 Mar 2023 21:17:28 +0100 Subject: [PATCH 010/100] automatically updating chat-app/client/TestData/schema-snapshot.json --- build.fsx | 40 + .../client/TestData/schema-snapshot.json | 2576 ++++++++++++++++- 2 files changed, 2615 insertions(+), 1 deletion(-) diff --git a/build.fsx b/build.fsx index dc08e5a2d..50de2bcc2 100644 --- a/build.fsx +++ b/build.fsx @@ -117,6 +117,7 @@ let runTests (project : string) = project let starWarsServerStream = StreamRef.Empty +let chatAppServerStream = StreamRef.Empty Target.create "StartStarWarsServer" <| fun _ -> Target.activateFinal "StopStarWarsServer" @@ -134,6 +135,24 @@ Target.createFinal "StopStarWarsServer" <| fun _ -> with e -> printfn "%s" e.Message +Target.create "StartChatAppServer" <| fun _ -> + Target.activateFinal "StopChatAppServer" + + let project = + "samples" + "chat-app" + "server" + "FSharp.Data.GraphQL.Samples.ChatApp.fsproj" + + startGraphQLServer project 8087 chatAppServerStream + +Target.createFinal "StopChatAppServer" <| fun _ -> + try + chatAppServerStream.Value.Write ([| 0uy |], 0, 1) + with e -> + printfn "%s" e.Message + + let integrationServerStream = StreamRef.Empty Target.create "StartIntegrationServer" <| fun _ -> @@ -170,6 +189,24 @@ Target.create "UpdateIntrospectionFile" <| fun _ -> }).Wait() client.Dispose() +Target.create "UpdateChatAppSchemaSnapshotFile" <| fun _ -> + let client = new HttpClient () + (task{ + let! result = client.GetAsync("http://localhost:8087") + let! contentStream = result.Content.ReadAsStreamAsync() + let! jsonDocument = JsonDocument.ParseAsync contentStream + let file = new FileStream("samples/chat-app/client/TestData/schema-snapshot.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() + Target.create "RunUnitTests" <| fun _ -> runTests "tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj" @@ -276,6 +313,9 @@ Target.create "PackAll" ignore ==> "StartIntegrationServer" ==> "UpdateIntrospectionFile" ==> "RunIntegrationTests" + ==> "StartChatAppServer" + ==> "UpdateChatAppSchemaSnapshotFile" + ==> "StopChatAppServer" ==> "All" =?> ("GenerateDocs", Environment.environVar "APPVEYOR" = "True") diff --git a/samples/chat-app/client/TestData/schema-snapshot.json b/samples/chat-app/client/TestData/schema-snapshot.json index 4e086b01e..1b52ce37b 100644 --- a/samples/chat-app/client/TestData/schema-snapshot.json +++ b/samples/chat-app/client/TestData/schema-snapshot.json @@ -1 +1,2575 @@ -{"data":{"__schema":{"queryType":{"name":"Query"},"mutationType":{"name":"Mutation"},"subscriptionType":{"name":"Subscription"},"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 String 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 ID 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":"Date","description":"The \u0060Date\u0060 scalar type represents a Date value with Time component. The Date 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":"URI","description":"The \u0060URI\u0060 scalar type represents a string resource identifier compatible with URI standard. The URI 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":null,"fields":[{"name":"organizations","description":"gets all available organizations","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Organization","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Organization","description":"An organization as seen from the outside","fields":[{"name":"chatRooms","description":"chat rooms in this organization","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatRoom","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"id","description":"the organization\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"members","description":"members of this organization","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Member","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the organization\u0027s name","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":"ChatRoom","description":"A chat room as viewed from the outside","fields":[{"name":"id","description":"the chat room\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"members","description":"the members in the chat room","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatMember","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the chat room\u0027s name","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":"SCALAR","name":"Guid","description":"The \u0060Guid\u0060 scalar type represents a Globaly 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":"OBJECT","name":"ChatMember","description":"A chat member is an organization member participating in a chat room","fields":[{"name":"id","description":"the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the member\u0027s name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":"the member\u0027s role in the chat","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"MemberRoleInChat","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"MemberRoleInChat","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"ChatAdmin","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"ChatGuest","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"Member","description":"An organization member","fields":[{"name":"id","description":"the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the member\u0027s name","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":"Subscription","description":null,"fields":[{"name":"chatRoomEvents","description":"events related to a specific chat room","args":[{"name":"chatRoomId","description":"the ID of the chat room to listen to events from","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatRoomEvent","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"ChatRoomEvent","description":"Something that happened in the chat room, like a new message sent","fields":[{"name":"chatRoomId","description":"the ID of the chat room in which the event happened","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"specificData","description":"the event\u0027s specific data","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"UNION","name":"ChatRoomSpecificEvent","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"time","description":"the time the message was received at the server","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Date","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"ChatRoomSpecificEvent","description":"data which is specific to a certain type of event","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"NewMessage","ofType":null},{"kind":"OBJECT","name":"EditedMessage","ofType":null},{"kind":"OBJECT","name":"DeletedMessage","ofType":null},{"kind":"OBJECT","name":"MemberJoined","ofType":null},{"kind":"OBJECT","name":"MemberLeft","ofType":null}]},{"kind":"OBJECT","name":"NewMessage","description":"a new public message has been sent in the chat room","fields":[{"name":"authorId","description":"the member ID of the message\u0027s author","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"chatRoomId","description":"the ID of the chat room the message belongs to","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"date","description":"the time the message was received at the server","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Date","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"id","description":"the message\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"text","description":"the message\u0027s text","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":"EditedMessage","description":"a public message of the chat room has been edited","fields":[{"name":"authorId","description":"the member ID of the message\u0027s author","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"chatRoomId","description":"the ID of the chat room the message belongs to","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"date","description":"the time the message was received at the server","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Date","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"id","description":"the message\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"text","description":"the message\u0027s text","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":"DeletedMessage","description":"a public message of the chat room has been deleted","fields":[{"name":"messageId","description":"this is the message ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"MemberJoined","description":"a member has joined the chat","fields":[{"name":"memberId","description":"this is the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"memberName","description":"this is the member\u0027s name","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":"MemberLeft","description":"a member has left the chat","fields":[{"name":"memberId","description":"this is the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"memberName","description":"this is the member\u0027s name","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":"Mutation","description":null,"fields":[{"name":"createChatRoom","description":"creates a new chat room for a user","args":[{"name":"organizationId","description":"the ID of the organization in which the chat room will be created","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"name","description":"the chat room\u0027s name","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatRoomForMember","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deleteChatMessage","description":null,"args":[{"name":"organizationId","description":"the ID of the organization the chat room and member are in","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"chatRoomId","description":"the chat room\u0027s ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"messageId","description":"the existing message\u0027s ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"editChatMessage","description":null,"args":[{"name":"organizationId","description":"the ID of the organization the chat room and member are in","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"chatRoomId","description":"the chat room\u0027s ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"messageId","description":"the existing message\u0027s ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"text","description":"the chat message\u0027s contents","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"enterChatRoom","description":"makes a member enter a chat room","args":[{"name":"organizationId","description":"the ID of the organization the chat room and member are in","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"chatRoomId","description":"the ID of the chat room","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatRoomForMember","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"enterOrganization","description":"makes a new member enter an organization","args":[{"name":"organizationId","description":"the ID of the organization","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"member","description":"the new member\u0027s name","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"OrganizationForMember","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"leaveChatRoom","description":"makes a member leave a chat room","args":[{"name":"organizationId","description":"the ID of the organization the chat room and member are in","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"chatRoomId","description":"the ID of the chat room","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"sendChatMessage","description":null,"args":[{"name":"organizationId","description":"the ID of the organization the chat room and member are in","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"chatRoomId","description":"the chat room\u0027s ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"memberId","description":"the member\u0027s private ID","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"defaultValue":null},{"name":"text","description":"the chat message\u0027s contents","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"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":"ChatRoomForMember","description":"A chat room as viewed by a chat room member","fields":[{"name":"id","description":"the chat room\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"meAsAChatMember","description":"the chat member that queried the details","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"MeAsAChatMember","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the chat room\u0027s name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"otherChatMembers","description":"the chat members excluding the one who queried the details","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatMember","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"MeAsAChatMember","description":"A chat member is an organization member participating in a chat room","fields":[{"name":"id","description":"the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the member\u0027s name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"privId","description":"the member\u0027s private ID used for authenticating their requests","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":"the member\u0027s role in the chat","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"MemberRoleInChat","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"OrganizationForMember","description":"An organization as seen by one of the organization\u0027s members","fields":[{"name":"chatRooms","description":"chat rooms in this organization","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"ChatRoom","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"id","description":"the organization\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"meAsAMember","description":"the member that queried the details","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"MeAsAMember","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the organization\u0027s name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"otherMembers","description":"members of this organization","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Member","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"MeAsAMember","description":"An organization member","fields":[{"name":"id","description":"the member\u0027s ID","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":"the member\u0027s name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"privId","description":"the member\u0027s private ID used for authenticating their requests","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Guid","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"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":[]}]}},"documentId":-412077120,"errors":[]} \ No newline at end of file +{ + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": { + "name": "Subscription" + }, + "types": [ + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` 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 `String` scalar type represents textual data, represented as UTF-8 character sequences. The String 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 `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Float", + "description": "The `Float` 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 `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID 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 `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Date", + "description": "The `Date` scalar type represents a Date value with Time component. The Date 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": "URI", + "description": "The `URI` scalar type represents a string resource identifier compatible with URI standard. The URI 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’s 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 `__TypeKind` 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. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` 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": null, + "fields": [ + { + "name": "organizations", + "description": "gets all available organizations", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Organization", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Organization", + "description": "An organization as seen from the outside", + "fields": [ + { + "name": "chatRooms", + "description": "chat rooms in this organization", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ChatRoom", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "the organization's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "members", + "description": "members of this organization", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Member", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "the organization's name", + "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": "ChatRoom", + "description": "A chat room as viewed from the outside", + "fields": [ + { + "name": "id", + "description": "the chat room's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "members", + "description": "the members in the chat room", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ChatMember", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "the chat room's name", + "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": "SCALAR", + "name": "Guid", + "description": "The `Guid` scalar type represents a Globaly Unique Identifier value. It's a 128-bit long byte key, that can be serialized to string.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ChatMember", + "description": "A chat member is an organization member participating in a chat room", + "fields": [ + { + "name": "id", + "description": "the member's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "the member's name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "role", + "description": "the member's role in the chat", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "MemberRoleInChat", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "MemberRoleInChat", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ChatAdmin", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ChatGuest", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Member", + "description": "An organization member", + "fields": [ + { + "name": "id", + "description": "the member's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "the member's name", + "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": "Subscription", + "description": null, + "fields": [ + { + "name": "chatRoomEvents", + "description": "events related to a specific chat room", + "args": [ + { + "name": "chatRoomId", + "description": "the ID of the chat room to listen to events from", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "memberId", + "description": "the member's private ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ChatRoomEvent", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ChatRoomEvent", + "description": "Something that happened in the chat room, like a new message sent", + "fields": [ + { + "name": "chatRoomId", + "description": "the ID of the chat room in which the event happened", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specificData", + "description": "the event's specific data", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "UNION", + "name": "ChatRoomSpecificEvent", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "time", + "description": "the time the message was received at the server", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "ChatRoomSpecificEvent", + "description": "data which is specific to a certain type of event", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "NewMessage", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "EditedMessage", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "DeletedMessage", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "MemberJoined", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "MemberLeft", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "NewMessage", + "description": "a new public message has been sent in the chat room", + "fields": [ + { + "name": "authorId", + "description": "the member ID of the message's author", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "chatRoomId", + "description": "the ID of the chat room the message belongs to", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "date", + "description": "the time the message was received at the server", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "the message's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "text", + "description": "the message's text", + "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": "EditedMessage", + "description": "a public message of the chat room has been edited", + "fields": [ + { + "name": "authorId", + "description": "the member ID of the message's author", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "chatRoomId", + "description": "the ID of the chat room the message belongs to", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "date", + "description": "the time the message was received at the server", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "the message's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "text", + "description": "the message's text", + "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": "DeletedMessage", + "description": "a public message of the chat room has been deleted", + "fields": [ + { + "name": "messageId", + "description": "this is the message ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MemberJoined", + "description": "a member has joined the chat", + "fields": [ + { + "name": "memberId", + "description": "this is the member's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "memberName", + "description": "this is the member's name", + "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": "MemberLeft", + "description": "a member has left the chat", + "fields": [ + { + "name": "memberId", + "description": "this is the member's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "memberName", + "description": "this is the member's name", + "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": "Mutation", + "description": null, + "fields": [ + { + "name": "createChatRoom", + "description": "creates a new chat room for a user", + "args": [ + { + "name": "organizationId", + "description": "the ID of the organization in which the chat room will be created", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "memberId", + "description": "the member's private ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "name", + "description": "the chat room's name", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ChatRoomForMember", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteChatMessage", + "description": null, + "args": [ + { + "name": "organizationId", + "description": "the ID of the organization the chat room and member are in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "chatRoomId", + "description": "the chat room's ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "memberId", + "description": "the member's private ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "messageId", + "description": "the existing message's ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "editChatMessage", + "description": null, + "args": [ + { + "name": "organizationId", + "description": "the ID of the organization the chat room and member are in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "chatRoomId", + "description": "the chat room's ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "memberId", + "description": "the member's private ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "messageId", + "description": "the existing message's ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "text", + "description": "the chat message's contents", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enterChatRoom", + "description": "makes a member enter a chat room", + "args": [ + { + "name": "organizationId", + "description": "the ID of the organization the chat room and member are in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "chatRoomId", + "description": "the ID of the chat room", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "memberId", + "description": "the member's private ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ChatRoomForMember", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enterOrganization", + "description": "makes a new member enter an organization", + "args": [ + { + "name": "organizationId", + "description": "the ID of the organization", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "member", + "description": "the new member's name", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "OrganizationForMember", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "leaveChatRoom", + "description": "makes a member leave a chat room", + "args": [ + { + "name": "organizationId", + "description": "the ID of the organization the chat room and member are in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "chatRoomId", + "description": "the ID of the chat room", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "memberId", + "description": "the member's private ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sendChatMessage", + "description": null, + "args": [ + { + "name": "organizationId", + "description": "the ID of the organization the chat room and member are in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "chatRoomId", + "description": "the chat room's ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "memberId", + "description": "the member's private ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "text", + "description": "the chat message's contents", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "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": "ChatRoomForMember", + "description": "A chat room as viewed by a chat room member", + "fields": [ + { + "name": "id", + "description": "the chat room's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meAsAChatMember", + "description": "the chat member that queried the details", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MeAsAChatMember", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "the chat room's name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "otherChatMembers", + "description": "the chat members excluding the one who queried the details", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ChatMember", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MeAsAChatMember", + "description": "A chat member is an organization member participating in a chat room", + "fields": [ + { + "name": "id", + "description": "the member's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "the member's name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "privId", + "description": "the member's private ID used for authenticating their requests", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "role", + "description": "the member's role in the chat", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "MemberRoleInChat", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "OrganizationForMember", + "description": "An organization as seen by one of the organization's members", + "fields": [ + { + "name": "chatRooms", + "description": "chat rooms in this organization", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ChatRoom", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "the organization's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meAsAMember", + "description": "the member that queried the details", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MeAsAMember", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "the organization's name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "otherMembers", + "description": "members of this organization", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Member", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MeAsAMember", + "description": "An organization member", + "fields": [ + { + "name": "id", + "description": "the member's ID", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "the member's name", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "privId", + "description": "the member's private ID used for authenticating their requests", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` 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 `if` 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": [] + } + ] + } + }, + "documentId": -976654609 +} \ No newline at end of file From d72500a7081c86d4762e39a180e3eb26092ac780 Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sat, 11 Mar 2023 21:21:22 +0100 Subject: [PATCH 011/100] Apply commit suggestion - use structural logging Co-authored-by: Andrii Chebukin --- .../GraphQLWebsocketMiddleware.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs index 85f3547c4..3853d5a86 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs @@ -107,7 +107,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti task { if not (socket.State = WebSocketState.Open) then if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace(sprintf "ignoring message to be sent via socket, since its state is not 'Open', but '%A'" socket.State) + logger.LogTrace("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'" socket.State) else let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions let segment = From 5226c163398e8f92d42454d8cfd85df07834d41d Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sat, 11 Mar 2023 21:24:51 +0100 Subject: [PATCH 012/100] Apply review suggestion - using `|> Task.WaitAll` instead of `|> Async.AwaitTask |> Async.RunSynchronously` Co-authored-by: Andrii Chebukin --- .../GraphQLWebsocketMiddleware.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs index 3853d5a86..16e6d1232 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs @@ -130,8 +130,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti (fun theOutput -> theOutput |> howToSendDataOnNext id - |> Async.AwaitTask - |> Async.RunSynchronously + |> Task.WaitAll ), onError = (fun ex -> From 13c495edd79ede04c45958093e9f6cdb9344cbaf Mon Sep 17 00:00:00 2001 From: valber Date: Sat, 11 Mar 2023 21:54:49 +0100 Subject: [PATCH 013/100] trying to use more structured logging (and fixing code) --- .../GraphQLWebsocketMiddleware.fs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs index 16e6d1232..4d59f1c48 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs @@ -107,7 +107,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti task { if not (socket.State = WebSocketState.Open) then if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'" socket.State) + logger.LogTrace("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", (socket.State.ToString())) else let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions let segment = @@ -116,12 +116,12 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti ) if not (socket.State = WebSocketState.Open) then if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace(sprintf "ignoring message to be sent via socket, since its state is not 'Open', but '%A'" socket.State) + logger.LogTrace("ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", (socket.State.ToString())) else do! socket.SendAsync(segment, WebSocketMessageType.Text, endOfMessage = true, cancellationToken = new CancellationToken()) if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace(sprintf "<- %A" message) + logger.LogTrace("<- Response: {response}", (message.ToString())) } let addClientSubscription (id : SubscriptionId) (jsonSerializerOptions) (socket) (howToSendDataOnNext: SubscriptionId -> Output -> Task) (streamSource: IObservable) (subscriptions : SubscriptionsDict) = @@ -134,7 +134,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti ), onError = (fun ex -> - logger.LogError(sprintf "[Error on subscription \"%s\"]: %s" id (ex.ToString())) + logger.LogError("[Error on subscription {id}]: {exception}", (id.ToString()), (ex.ToString())) ), onCompleted = (fun () -> @@ -219,13 +219,13 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti |> Option.map (fun payloadStr -> sprintf " with payload: %A" payloadStr) |> Option.defaultWith (fun () -> "") - let logMsgReceivedWithOptionalPayload optionalPayload msgAsStr = + let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) = if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace (sprintf "%s%s" msgAsStr (optionalPayload |> getStrAddendumOfOptionalPayload)) + logger.LogTrace ("{message}{messageaddendum}", msgAsStr, (optionalPayload |> getStrAddendumOfOptionalPayload)) - let logMsgWithIdReceived id msgAsStr = + let logMsgWithIdReceived (id : string) (msgAsStr : string) = if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace(sprintf "%s (id: %s)" msgAsStr id) + logger.LogTrace("{message} (id: {messageid})", msgAsStr, id) // <-------------- // <-- Helpers --| @@ -353,7 +353,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti |> waitForConnectionInitAndRespondToClient options.SerializerOptions options.SchemaExecutor options.WebsocketOptions.ConnectionInitTimeoutInMs match connectionInitResult with | Failure errMsg -> - logger.LogWarning(sprintf "%A" errMsg) + logger.LogWarning("{warningmsg}", (sprintf "%A" errMsg)) | Success _ -> let longRunningCancellationToken = (CancellationTokenSource.CreateLinkedTokenSource(ctx.RequestAborted, applicationLifetime.ApplicationStopping).Token) @@ -369,7 +369,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti |> safe_HandleMessages options.SerializerOptions options.SchemaExecutor options.RootFactory options.WebsocketOptions.CustomPingHandler with | ex -> - logger.LogError(sprintf "Unexpected exception \"%s\" in GraphQLWebsocketMiddleware. More:\n%s" (ex.GetType().Name) (ex.ToString())) + logger.LogError("Unexpected exception \"{exceptionname}\" in GraphQLWebsocketMiddleware. More:\n{exceptionstr}", (ex.GetType().Name), (ex.ToString())) else do! next.Invoke(ctx) } \ No newline at end of file From f3e85ccfea0d63a6d8da09f6ca758686494cf36c Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 12 Mar 2023 14:17:50 +0100 Subject: [PATCH 014/100] not calling .ToString() on objects that go into structured logging According to suggestion during code review. --- .../GraphQLWebsocketMiddleware.fs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs index 4d59f1c48..05c00ff33 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs @@ -107,7 +107,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti task { if not (socket.State = WebSocketState.Open) then if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", (socket.State.ToString())) + logger.LogTrace("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) else let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions let segment = @@ -116,12 +116,12 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti ) if not (socket.State = WebSocketState.Open) then if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace("ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", (socket.State.ToString())) + logger.LogTrace("ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) else do! socket.SendAsync(segment, WebSocketMessageType.Text, endOfMessage = true, cancellationToken = new CancellationToken()) if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace("<- Response: {response}", (message.ToString())) + logger.LogTrace("<- Response: {response}", message) } let addClientSubscription (id : SubscriptionId) (jsonSerializerOptions) (socket) (howToSendDataOnNext: SubscriptionId -> Output -> Task) (streamSource: IObservable) (subscriptions : SubscriptionsDict) = @@ -134,7 +134,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti ), onError = (fun ex -> - logger.LogError("[Error on subscription {id}]: {exception}", (id.ToString()), (ex.ToString())) + logger.LogError("[Error on subscription {id}]: {exceptionstr}", id, ex) ), onCompleted = (fun () -> @@ -285,7 +285,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior with | ex -> - logger.LogError (sprintf "Unexpected exception \"%s\" in GraphQLWebsocketMiddleware (handleMessages). More:\n%s" (ex.GetType().Name) (ex.ToString())) + logger.LogError("Unexpected exception \"{exceptionname}\" in GraphQLWebsocketMiddleware (handleMessages). More:\n{exceptionstr}", (ex.GetType().Name), ex) // at this point, only something really weird must have happened. // In order to avoid faulty state scenarios and unimagined damages, // just close the socket without further ado. @@ -369,7 +369,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti |> safe_HandleMessages options.SerializerOptions options.SchemaExecutor options.RootFactory options.WebsocketOptions.CustomPingHandler with | ex -> - logger.LogError("Unexpected exception \"{exceptionname}\" in GraphQLWebsocketMiddleware. More:\n{exceptionstr}", (ex.GetType().Name), (ex.ToString())) + logger.LogError("Unexpected exception \"{exceptionname}\" in GraphQLWebsocketMiddleware. More:\n{exceptionstr}", (ex.GetType().Name), ex) else do! next.Invoke(ctx) } \ No newline at end of file From fdc5289f490855e1738d7c030d23279d50976b8c Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sun, 19 Mar 2023 10:22:12 +0100 Subject: [PATCH 015/100] Using more concise Task.WaitAll As suggested during code review Co-authored-by: Andrii Chebukin --- .../GraphQLWebsocketMiddleware.fs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs index 05c00ff33..bb1e8b74a 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs @@ -302,9 +302,8 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti timerTokenSource.CancelAfter(connectionInitTimeoutInMs) let detonationRegistration = timerTokenSource.Token.Register(fun _ -> socket - |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.connectionTimeout, "Connection initialisation timeout") - |> Async.AwaitTask - |> Async.RunSynchronously + |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.connectionTimeout, "Connection initialization timeout") + |> Task.WaitAll ) let! connectionInitSucceeded = Task.Run((fun _ -> task { From 34471ab4b160d580589d8e4579b989addebb736d Mon Sep 17 00:00:00 2001 From: valber Date: Sat, 18 Mar 2023 22:06:22 +0100 Subject: [PATCH 016/100] ChatAppServer -> ChatServer According to suggestion during pull request code review --- build.fsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.fsx b/build.fsx index 50de2bcc2..85c53c0f4 100644 --- a/build.fsx +++ b/build.fsx @@ -117,7 +117,7 @@ let runTests (project : string) = project let starWarsServerStream = StreamRef.Empty -let chatAppServerStream = StreamRef.Empty +let chatServerStream = StreamRef.Empty Target.create "StartStarWarsServer" <| fun _ -> Target.activateFinal "StopStarWarsServer" @@ -135,8 +135,8 @@ Target.createFinal "StopStarWarsServer" <| fun _ -> with e -> printfn "%s" e.Message -Target.create "StartChatAppServer" <| fun _ -> - Target.activateFinal "StopChatAppServer" +Target.create "StartChatServer" <| fun _ -> + Target.activateFinal "StopChatServer" let project = "samples" @@ -144,11 +144,11 @@ Target.create "StartChatAppServer" <| fun _ -> "server" "FSharp.Data.GraphQL.Samples.ChatApp.fsproj" - startGraphQLServer project 8087 chatAppServerStream + startGraphQLServer project 8087 chatServerStream -Target.createFinal "StopChatAppServer" <| fun _ -> +Target.createFinal "StopChatServer" <| fun _ -> try - chatAppServerStream.Value.Write ([| 0uy |], 0, 1) + chatServerStream.Value.Write ([| 0uy |], 0, 1) with e -> printfn "%s" e.Message @@ -313,9 +313,9 @@ Target.create "PackAll" ignore ==> "StartIntegrationServer" ==> "UpdateIntrospectionFile" ==> "RunIntegrationTests" - ==> "StartChatAppServer" + ==> "StartChatServer" ==> "UpdateChatAppSchemaSnapshotFile" - ==> "StopChatAppServer" + ==> "StopChatServer" ==> "All" =?> ("GenerateDocs", Environment.environVar "APPVEYOR" = "True") From e427f555e8ec437034630ae251c7c55f9983073a Mon Sep 17 00:00:00 2001 From: valber Date: Sat, 18 Mar 2023 22:16:50 +0100 Subject: [PATCH 017/100] build.fsx: intr. helper func "updateIntrospectionFile" --- build.fsx | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/build.fsx b/build.fsx index 85c53c0f4..2a3e23034 100644 --- a/build.fsx +++ b/build.fsx @@ -171,13 +171,13 @@ Target.createFinal "StopIntegrationServer" <| fun _ -> with e -> printfn "%s" e.Message -Target.create "UpdateIntrospectionFile" <| fun _ -> +let updateIntrospectionFile (outputRelativePath : string) (url : string) (targetParameter: TargetParameter) = let client = new HttpClient () - (task{ - let! result = client.GetAsync("http://localhost:8086") + (task { + let! result = client.GetAsync(url) 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 file = new FileStream(outputRelativePath, 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) @@ -189,23 +189,12 @@ Target.create "UpdateIntrospectionFile" <| fun _ -> }).Wait() client.Dispose() -Target.create "UpdateChatAppSchemaSnapshotFile" <| fun _ -> - let client = new HttpClient () - (task{ - let! result = client.GetAsync("http://localhost:8087") - let! contentStream = result.Content.ReadAsStreamAsync() - let! jsonDocument = JsonDocument.ParseAsync contentStream - let file = new FileStream("samples/chat-app/client/TestData/schema-snapshot.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() +Target.create "UpdateIntrospectionFile" <| + ("http://localhost:8086" |> updateIntrospectionFile "tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json") + +Target.create "UpdateChatAppSchemaSnapshotFile" <| + ("http://localhost:8087" |> updateIntrospectionFile "samples/chat-app/client/TestData/schema-snapshot.json") + Target.create "RunUnitTests" <| fun _ -> runTests "tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj" From ea5be3426e5b2e588ecaf0e4484a1bed9dc8aba2 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 09:54:31 +0100 Subject: [PATCH 018/100] renamed AppInfrastructure to AspNetCore According to suggestions during code review. --- FSharp.Data.GraphQL.sln | 2 +- README.md | 2 +- build.fsx | 6 +++--- samples/chat-app/client/TestData/schema-snapshot.json | 2 +- .../server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj | 2 +- samples/chat-app/server/Program.fs | 4 ++-- samples/chat-app/server/Schema.fs | 2 +- .../FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj | 2 +- samples/star-wars-api/Startup.fs | 4 ++-- .../Exceptions.fs | 2 +- .../FSharp.Data.GraphQL.Server.AspNetCore.fsproj} | 0 .../Giraffe/HttpHandlers.fs | 6 +++--- .../GraphQLOptions.fs | 2 +- .../GraphQLSubscriptionsManagement.fs | 2 +- .../GraphQLWebsocketMiddleware.fs | 4 ++-- .../Messages.fs | 2 +- .../README.md | 8 ++++---- .../Rop/Rop.fs | 2 +- .../Rop/RopAsync.fs | 2 +- .../Serialization/GraphQLQueryDecoding.fs | 2 +- .../Serialization/JsonConverters.fs | 2 +- .../StartupExtensions.fs | 2 +- .../InvalidMessageTests.fs | 10 +++++----- .../SerializationTests.fs | 4 ++-- .../{AppInfrastructure => AspNetCore}/TestSchema.fs | 2 +- .../FSharp.Data.GraphQL.Tests.fsproj | 8 ++++---- 26 files changed, 43 insertions(+), 43 deletions(-) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/Exceptions.fs (63%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure/FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj => FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj} (100%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/Giraffe/HttpHandlers.fs (97%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/GraphQLOptions.fs (90%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/GraphQLSubscriptionsManagement.fs (95%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/GraphQLWebsocketMiddleware.fs (99%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/Messages.fs (96%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/README.md (93%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/Rop/Rop.fs (98%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/Rop/RopAsync.fs (88%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/Serialization/GraphQLQueryDecoding.fs (98%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/Serialization/JsonConverters.fs (99%) rename src/{FSharp.Data.GraphQL.Server.AppInfrastructure => FSharp.Data.GraphQL.Server.AspNetCore}/StartupExtensions.fs (96%) rename tests/FSharp.Data.GraphQL.Tests/{AppInfrastructure => AspNetCore}/InvalidMessageTests.fs (90%) rename tests/FSharp.Data.GraphQL.Tests/{AppInfrastructure => AspNetCore}/SerializationTests.fs (97%) rename tests/FSharp.Data.GraphQL.Tests/{AppInfrastructure => AspNetCore}/TestSchema.fs (99%) diff --git a/FSharp.Data.GraphQL.sln b/FSharp.Data.GraphQL.sln index ca306c6b3..722e3dd5d 100644 --- a/FSharp.Data.GraphQL.sln +++ b/FSharp.Data.GraphQL.sln @@ -45,7 +45,7 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Shared" EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Server", "src\FSharp.Data.GraphQL.Server\FSharp.Data.GraphQL.Server.fsproj", "{474179D3-0090-49E9-88F8-2971C0966077}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.GraphQL.Server.AppInfrastructure", "src\FSharp.Data.GraphQL.Server.AppInfrastructure\FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj", "{554A6833-1E72-41B4-AAC1-C19371EC061B}" +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.GraphQL.Server.AspNetCore", "src\FSharp.Data.GraphQL.Server.AspNetCore\FSharp.Data.GraphQL.Server.AspNetCore.fsproj", "{554A6833-1E72-41B4-AAC1-C19371EC061B}" EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Server.Relay", "src\FSharp.Data.GraphQL.Server.Relay\FSharp.Data.GraphQL.Server.Relay.fsproj", "{E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}" EndProject diff --git a/README.md b/README.md index bf1caf2b2..7841d23d2 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ It's type safe. Things like invalid fields or invalid return types will be check ### ASP.NET / Giraffe / Websocket (for GraphQL subscriptions) usage -→ See the [AppInfrastructure/README.md](src/FSharp.Data.GraphQL.Server.AppInfrastructure/README.md) +→ See the [AspNetCore/README.md](src/FSharp.Data.GraphQL.Server.AspNetCore/README.md) ## Demos diff --git a/build.fsx b/build.fsx index 2a3e23034..19cae05e2 100644 --- a/build.fsx +++ b/build.fsx @@ -275,7 +275,7 @@ Target.create "PublishMiddleware" <| fun _ -> publishPackage "Server.Middleware" Target.create "PublishShared" <| fun _ -> publishPackage "Shared" -Target.create "PublishAppInfrastructure" <| fun _ -> publishPackage "Server.AppInfrastructure" +Target.create "PublishAspNetCore" <| fun _ -> publishPackage "Server.AspNetCore" Target.create "PackServer" <| fun _ -> pack "Server" @@ -285,7 +285,7 @@ Target.create "PackMiddleware" <| fun _ -> pack "Server.Middleware" Target.create "PackShared" <| fun _ -> pack "Shared" -Target.create "PackAppInfrastructure" <| fun _ -> pack "Server.AppInfrastructure" +Target.create "PackAspNetCore" <| fun _ -> pack "Server.AspNetCore" // -------------------------------------------------------------------------------------- @@ -315,7 +315,7 @@ Target.create "PackAll" ignore ==> "PackServer" ==> "PackClient" ==> "PackMiddleware" - ==> "PackAppInfrastructure" + ==> "PackAspNetCore" ==> "PackAll" Target.runOrDefaultWithArguments "All" diff --git a/samples/chat-app/client/TestData/schema-snapshot.json b/samples/chat-app/client/TestData/schema-snapshot.json index 1b52ce37b..071304ace 100644 --- a/samples/chat-app/client/TestData/schema-snapshot.json +++ b/samples/chat-app/client/TestData/schema-snapshot.json @@ -2571,5 +2571,5 @@ ] } }, - "documentId": -976654609 + "documentId": -631396295 } \ No newline at end of file diff --git a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj index ee414838f..4abcca81b 100644 --- a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj +++ b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj @@ -12,7 +12,7 @@ - + diff --git a/samples/chat-app/server/Program.fs b/samples/chat-app/server/Program.fs index c78db94a8..1161dbe11 100644 --- a/samples/chat-app/server/Program.fs +++ b/samples/chat-app/server/Program.fs @@ -1,8 +1,8 @@ namespace FSharp.Data.GraphQL.Samples.ChatApp open Giraffe -open FSharp.Data.GraphQL.Server.AppInfrastructure -open FSharp.Data.GraphQL.Server.AppInfrastructure.Giraffe +open FSharp.Data.GraphQL.Server.AspNetCore +open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe open System open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Server.Kestrel.Core diff --git a/samples/chat-app/server/Schema.fs b/samples/chat-app/server/Schema.fs index 13e5b609b..95af19c42 100644 --- a/samples/chat-app/server/Schema.fs +++ b/samples/chat-app/server/Schema.fs @@ -112,7 +112,7 @@ module MapFrom = module Schema = - open FSharp.Data.GraphQL.Server.AppInfrastructure.Rop + open FSharp.Data.GraphQL.Server.AspNetCore.Rop let validationException_Member_With_This_Name_Already_Exists (theName : string) = GraphQLException(sprintf "member with name \"%s\" already exists" theName) diff --git a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj index 5e9073532..be0e1024a 100644 --- a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj +++ b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj @@ -25,7 +25,7 @@ - + diff --git a/samples/star-wars-api/Startup.fs b/samples/star-wars-api/Startup.fs index 6601da30f..d765aaac6 100644 --- a/samples/star-wars-api/Startup.fs +++ b/samples/star-wars-api/Startup.fs @@ -1,8 +1,8 @@ namespace FSharp.Data.GraphQL.Samples.StarWarsApi open Giraffe -open FSharp.Data.GraphQL.Server.AppInfrastructure.Giraffe -open FSharp.Data.GraphQL.Server.AppInfrastructure +open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe +open FSharp.Data.GraphQL.Server.AspNetCore open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Server.Kestrel.Core diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Exceptions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs similarity index 63% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/Exceptions.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs index 7286292ef..1d1cef7c2 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Exceptions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs @@ -1,4 +1,4 @@ -namespace FSharp.Data.GraphQL.Server.AppInfrastructure +namespace FSharp.Data.GraphQL.Server.AspNetCore type InvalidMessageException (explanation : string) = inherit System.Exception(explanation) \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj similarity index 100% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/FSharp.Data.GraphQL.Server.AppInfrastructure.fsproj rename to src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs similarity index 97% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index ab0395b27..dc167b508 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -1,10 +1,10 @@ -namespace FSharp.Data.GraphQL.Server.AppInfrastructure.Giraffe +namespace FSharp.Data.GraphQL.Server.AspNetCore.Giraffe open FSharp.Data.GraphQL.Execution open FSharp.Data.GraphQL open Giraffe -open FSharp.Data.GraphQL.Server.AppInfrastructure -open FSharp.Data.GraphQL.Server.AppInfrastructure.Rop +open FSharp.Data.GraphQL.Server.AspNetCore +open FSharp.Data.GraphQL.Server.AspNetCore.Rop open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLOptions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs similarity index 90% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLOptions.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs index d8c1a4ef3..a85484fd5 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLOptions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs @@ -1,4 +1,4 @@ -namespace FSharp.Data.GraphQL.Server.AppInfrastructure +namespace FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL open System diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLSubscriptionsManagement.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs similarity index 95% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLSubscriptionsManagement.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs index d3de048f4..d2d589d86 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLSubscriptionsManagement.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs @@ -1,4 +1,4 @@ -namespace FSharp.Data.GraphQL.Server.AppInfrastructure +namespace FSharp.Data.GraphQL.Server.AspNetCore module internal GraphQLSubscriptionsManagement = let addSubscription (id : SubscriptionId, unsubscriber : SubscriptionUnsubscriber, onUnsubscribe : OnUnsubscribeAction) diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs similarity index 99% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index bb1e8b74a..70bed178c 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -1,8 +1,8 @@ -namespace FSharp.Data.GraphQL.Server.AppInfrastructure +namespace FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL open Microsoft.AspNetCore.Http -open FSharp.Data.GraphQL.Server.AppInfrastructure.Rop +open FSharp.Data.GraphQL.Server.AspNetCore.Rop open System open System.Collections.Generic open System.Net.WebSockets diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Messages.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs similarity index 96% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/Messages.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs index f40c8fcd4..149696849 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Messages.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs @@ -1,4 +1,4 @@ -namespace FSharp.Data.GraphQL.Server.AppInfrastructure +namespace FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL.Execution open FSharp.Data.GraphQL.Types diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/README.md b/src/FSharp.Data.GraphQL.Server.AspNetCore/README.md similarity index 93% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/README.md rename to src/FSharp.Data.GraphQL.Server.AspNetCore/README.md index 25f2d753f..fbec7edb0 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/README.md +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/README.md @@ -7,8 +7,8 @@ In a `Startup` class... namespace MyApp open Giraffe -open FSharp.Data.GraphQL.Server.AppInfrastructure.Giraffe -open FSharp.Data.GraphQL.Server.AppInfrastructure +open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe +open FSharp.Data.GraphQL.Server.AspNetCore open Microsoft.AspNetCore.Server.Kestrel.Core open Microsoft.AspNetCore.Builder open Microsoft.Extensions.Configuration @@ -93,8 +93,8 @@ Don't forget to notify subscribers about new values: Finally run the server (e.g. make it listen at `localhost:8086`). -There's a demo chat application backend in the `samples/chat-app` folder that showcases the use of `FSharp.Data.GraphQL.Server.AppInfrastructure` in a real-time application scenario, that is: with usage of GraphQL subscriptions (but not only). -The tried and trusted `star-wars-api` also shows how to use subscriptions, but is a more basic example. As a side note, the implementation in `star-wars-api` was used as a starting point for the development of `FSharp.Data.GraphQL.Server.AppInfrastructure`. +There's a demo chat application backend in the `samples/chat-app` folder that showcases the use of `FSharp.Data.GraphQL.Server.AspNetCore` in a real-time application scenario, that is: with usage of GraphQL subscriptions (but not only). +The tried and trusted `star-wars-api` also shows how to use subscriptions, but is a more basic example. As a side note, the implementation in `star-wars-api` was used as a starting point for the development of `FSharp.Data.GraphQL.Server.AspNetCore`. ### Client Using your favorite (or not :)) client library (e.g.: [Apollo Client](https://www.apollographql.com/docs/react/get-started), [Relay](https://relay.dev), [Strawberry Shake](https://chillicream.com/docs/strawberryshake/v13), [elm-graphql](https://github.com/dillonkearns/elm-graphql) ❤️), just point to `localhost:8086/graphql` (as per the example above) and, as long as the client implements the `graphql-transport-ws` subprotocol, subscriptions should work. diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/Rop.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/Rop.fs similarity index 98% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/Rop.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/Rop.fs index 6a3f8900e..808ac0594 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/Rop.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/Rop.fs @@ -1,6 +1,6 @@ // https://fsharpforfunandprofit.com/rop/ -module FSharp.Data.GraphQL.Server.AppInfrastructure.Rop +module FSharp.Data.GraphQL.Server.AspNetCore.Rop /// A Result is a success or failure /// The Success case has a success value, plus a list of messages diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/RopAsync.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/RopAsync.fs similarity index 88% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/RopAsync.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/RopAsync.fs index ffa84e307..960fa77b7 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Rop/RopAsync.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/RopAsync.fs @@ -1,4 +1,4 @@ -module FSharp.Data.GraphQL.Server.AppInfrastructure.RopAsync +module FSharp.Data.GraphQL.Server.AspNetCore.RopAsync open Rop open System.Threading.Tasks diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/GraphQLQueryDecoding.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs similarity index 98% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/GraphQLQueryDecoding.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs index 65e03f4f2..0b4e386d6 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/GraphQLQueryDecoding.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs @@ -1,4 +1,4 @@ -namespace FSharp.Data.GraphQL.Server.AppInfrastructure +namespace FSharp.Data.GraphQL.Server.AspNetCore module GraphQLQueryDecoding = open FSharp.Data.GraphQL diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs similarity index 99% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/JsonConverters.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index 76603ee9c..f866ff898 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -1,4 +1,4 @@ -namespace FSharp.Data.GraphQL.Server.AppInfrastructure +namespace FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL open Rop diff --git a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs similarity index 96% rename from src/FSharp.Data.GraphQL.Server.AppInfrastructure/StartupExtensions.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index 798d97a5f..ccfa00aa8 100644 --- a/src/FSharp.Data.GraphQL.Server.AppInfrastructure/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -1,4 +1,4 @@ -namespace FSharp.Data.GraphQL.Server.AppInfrastructure +namespace FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL open Microsoft.AspNetCore.Builder diff --git a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs similarity index 90% rename from tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs rename to tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs index b7e20a06a..f21cf7f58 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/InvalidMessageTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs @@ -1,7 +1,7 @@ -module FSharp.Data.GraphQL.Tests.AppInfrastructure.InvalidMessageTests +module FSharp.Data.GraphQL.Tests.AspNetCore.InvalidMessageTests -open FSharp.Data.GraphQL.Tests.AppInfrastructure -open FSharp.Data.GraphQL.Server.AppInfrastructure +open FSharp.Data.GraphQL.Tests.AspNetCore +open FSharp.Data.GraphQL.Server.AspNetCore open System.Text.Json open Xunit @@ -77,7 +77,7 @@ let ``payload type of number in subscribe message`` () = "payload": 42 } """ - |> willResultInInvalidMessage "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AppInfrastructure.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 2." + |> willResultInInvalidMessage "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AspNetCore.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 2." [] let ``no id in subscribe message`` () = @@ -98,7 +98,7 @@ let ``string payload wrongly used in subscribe`` () = "payload": "{\"query\": \"subscription { watchMoon(id: \\\"1\\\") { id name isMoon } }\"}" } """ - |> willResultInInvalidMessage "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AppInfrastructure.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 79." + |> willResultInInvalidMessage "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AspNetCore.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 79." [] let ``id is incorrectly a number in a subscribe message`` () = diff --git a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/SerializationTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs similarity index 97% rename from tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/SerializationTests.fs rename to tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs index aaac657a5..eabd0b582 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/SerializationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs @@ -1,7 +1,7 @@ -module FSharp.Data.GraphQL.Tests.AppInfrastructure.SerializationTests +module FSharp.Data.GraphQL.Tests.AspNetCore.SerializationTests open FSharp.Data.GraphQL.Ast -open FSharp.Data.GraphQL.Server.AppInfrastructure +open FSharp.Data.GraphQL.Server.AspNetCore open System.Text.Json open Xunit diff --git a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/TestSchema.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs similarity index 99% rename from tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/TestSchema.fs rename to tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs index 78149f649..f2f2bbc42 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AppInfrastructure/TestSchema.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs @@ -1,4 +1,4 @@ -namespace FSharp.Data.GraphQL.Tests.AppInfrastructure +namespace FSharp.Data.GraphQL.Tests.AspNetCore open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types 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 012de8e57..f1918db34 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -54,9 +54,9 @@ - - - + + + @@ -69,7 +69,7 @@ - + \ No newline at end of file From bf672ca4761b68df034395b65fee7c10dfa204cd Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 14:24:13 +0100 Subject: [PATCH 019/100] removing logger.IsEnabled where it is superfluous According to suggestion during code review. --- .../GraphQLWebsocketMiddleware.fs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 70bed178c..606a13010 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -106,8 +106,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) = task { if not (socket.State = WebSocketState.Open) then - if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) + logger.LogTrace("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) else let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions let segment = @@ -115,13 +114,11 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti System.Text.Encoding.UTF8.GetBytes(serializedMessage) ) if not (socket.State = WebSocketState.Open) then - if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace("ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) + logger.LogTrace("ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) else do! socket.SendAsync(segment, WebSocketMessageType.Text, endOfMessage = true, cancellationToken = new CancellationToken()) - if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace("<- Response: {response}", message) + logger.LogTrace("<- Response: {response}", message) } let addClientSubscription (id : SubscriptionId) (jsonSerializerOptions) (socket) (howToSendDataOnNext: SubscriptionId -> Output -> Task) (streamSource: IObservable) (subscriptions : SubscriptionsDict) = @@ -220,12 +217,10 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti |> Option.defaultWith (fun () -> "") let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) = - if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace ("{message}{messageaddendum}", msgAsStr, (optionalPayload |> getStrAddendumOfOptionalPayload)) + logger.LogTrace ("{message}{messageaddendum}", msgAsStr, (optionalPayload |> getStrAddendumOfOptionalPayload)) let logMsgWithIdReceived (id : string) (msgAsStr : string) = - if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace("{message} (id: {messageid})", msgAsStr, id) + logger.LogTrace("{message} (id: {messageid})", msgAsStr, id) // <-------------- // <-- Helpers --| @@ -240,8 +235,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti let! receivedMessage = safe_Receive() match receivedMessage with | None -> - if logger.IsEnabled(LogLevel.Trace) then - logger.LogTrace(sprintf "Websocket socket received empty message! (socket state = %A)" socket.State) + logger.LogTrace("Websocket socket received empty message! (socket state = {socketstate})", socket.State) | Some msg -> match msg with | Failure failureMsgs -> From c390676bea1e840f00fc0d5d829c12918c4199c8 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 14:32:17 +0100 Subject: [PATCH 020/100] Using better suited LogError overload and are shorter error message According to suggestion during code review. --- .../GraphQLWebsocketMiddleware.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 606a13010..9a44bd36f 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -279,7 +279,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior with | ex -> - logger.LogError("Unexpected exception \"{exceptionname}\" in GraphQLWebsocketMiddleware (handleMessages). More:\n{exceptionstr}", (ex.GetType().Name), ex) + logger.LogError(ex, "Cannot handle a message; dropping a websocket connection") // at this point, only something really weird must have happened. // In order to avoid faulty state scenarios and unimagined damages, // just close the socket without further ado. From d8a561952dcbb5ebbf7c6f871e0b193e29176d22 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 14:57:01 +0100 Subject: [PATCH 021/100] CancellationToken.None instead of new CancellationToken() According to suggestion during code review. --- .../GraphQLWebsocketMiddleware.fs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 9a44bd36f..3584120f3 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -116,7 +116,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti if not (socket.State = WebSocketState.Open) then logger.LogTrace("ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) else - do! socket.SendAsync(segment, WebSocketMessageType.Text, endOfMessage = true, cancellationToken = new CancellationToken()) + do! socket.SendAsync(segment, WebSocketMessageType.Text, endOfMessage = true, cancellationToken = CancellationToken.None) logger.LogTrace("<- Response: {response}", message) } @@ -153,7 +153,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti task { if theSocket |> canCloseSocket then - do! theSocket.CloseAsync(code, message, new CancellationToken()) + do! theSocket.CloseAsync(code, message, CancellationToken.None) else () } @@ -166,7 +166,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti // ----------> // Helpers --> // ----------> - let safe_ReceiveMessageViaSocket = receiveMessageViaSocket (new CancellationToken()) + let safe_ReceiveMessageViaSocket = receiveMessageViaSocket (CancellationToken.None) let safe_Send = sendMessageViaSocket serializerOptions socket let safe_Receive() = @@ -242,13 +242,13 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti "InvalidMessage" |> logMsgReceivedWithOptionalPayload None match failureMsgs |> List.head with | InvalidMessage (code, explanation) -> - do! socket.CloseAsync(enum code, explanation, new CancellationToken()) + do! socket.CloseAsync(enum code, explanation, CancellationToken.None) | Success (ConnectionInit p, _) -> "ConnectionInit" |> logMsgReceivedWithOptionalPayload p do! socket.CloseAsync( enum CustomWebSocketStatus.tooManyInitializationRequests, "too many initialization requests", - new CancellationToken()) + CancellationToken.None) | Success (ClientPing p, _) -> "ClientPing" |> logMsgReceivedWithOptionalPayload p match pingHandler with @@ -265,7 +265,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti do! socket.CloseAsync( enum CustomWebSocketStatus.subscriberAlreadyExists, sprintf "Subscriber for %s already exists" id, - new CancellationToken()) + CancellationToken.None) else let! planExecutionResult = executor.AsyncExecute(query.ExecutionPlan, root(), query.Variables) @@ -302,7 +302,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti let! connectionInitSucceeded = Task.Run((fun _ -> task { logger.LogDebug("Waiting for ConnectionInit...") - let! receivedMessage = receiveMessageViaSocket (new CancellationToken()) serializerOptions schemaExecutor socket + let! receivedMessage = receiveMessageViaSocket (CancellationToken.None) serializerOptions schemaExecutor socket match receivedMessage with | Some (Success (ConnectionInit payload, _)) -> logger.LogDebug("Valid connection_init received! Responding with ACK!") From a2b271ea023382bd738b08bcc9e7d1203947e374 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 15:46:17 +0100 Subject: [PATCH 022/100] Using a better suited logger.LogError overload and better phrasing According to suggestion during code review. --- .../GraphQLWebsocketMiddleware.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 3584120f3..69c0c049e 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -362,7 +362,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti |> safe_HandleMessages options.SerializerOptions options.SchemaExecutor options.RootFactory options.WebsocketOptions.CustomPingHandler with | ex -> - logger.LogError("Unexpected exception \"{exceptionname}\" in GraphQLWebsocketMiddleware. More:\n{exceptionstr}", (ex.GetType().Name), ex) + logger.LogError(ex, "Cannot handle Websocket message.") else do! next.Invoke(ctx) } \ No newline at end of file From 70ba89aaf6355dac020a3162634adfa60c69dd55 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 15:53:17 +0100 Subject: [PATCH 023/100] chat-app sample: moved FakePersistence to own file According to suggestion during code review --- ...FSharp.Data.GraphQL.Samples.ChatApp.fsproj | 1 + samples/chat-app/server/FakePersistence.fs | 39 +++++++++++++++++++ samples/chat-app/server/Schema.fs | 36 ----------------- 3 files changed, 40 insertions(+), 36 deletions(-) create mode 100644 samples/chat-app/server/FakePersistence.fs diff --git a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj index 4abcca81b..b9b712f71 100644 --- a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj +++ b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj @@ -7,6 +7,7 @@ + diff --git a/samples/chat-app/server/FakePersistence.fs b/samples/chat-app/server/FakePersistence.fs new file mode 100644 index 000000000..975761236 --- /dev/null +++ b/samples/chat-app/server/FakePersistence.fs @@ -0,0 +1,39 @@ +namespace FSharp.Data.GraphQL.Samples.ChatApp + +open System + +type FakePersistence() = + static let mutable _members = Map.empty + static let mutable _chatMembers = Map.empty + static let mutable _chatRoomMessages = Map.empty + static let mutable _chatRooms = Map.empty + static let mutable _organizations = + let newId = OrganizationId (Guid.Parse("51f823ef-2294-41dc-9f39-a4b9a237317a")) + ( newId, + { Organization_In_Db.Id = newId + Name = "Public" + Members = [] + ChatRooms = [] } + ) + |> List.singleton + |> Map.ofList + + static member Members + with get() = _members + and set(v) = _members <- v + + static member ChatMembers + with get() = _chatMembers + and set(v) = _chatMembers <- v + + static member ChatRoomMessages + with get() = _chatRoomMessages + and set(v) = _chatRoomMessages <- v + + static member ChatRooms + with get() = _chatRooms + and set(v) = _chatRooms <- v + + static member Organizations + with get() = _organizations + and set(v) = _organizations <- v \ No newline at end of file diff --git a/samples/chat-app/server/Schema.fs b/samples/chat-app/server/Schema.fs index 95af19c42..5f83a0541 100644 --- a/samples/chat-app/server/Schema.fs +++ b/samples/chat-app/server/Schema.fs @@ -7,42 +7,6 @@ open System type Root = { RequestId : string } -type FakePersistence() = - static let mutable _members = Map.empty - static let mutable _chatMembers = Map.empty - static let mutable _chatRoomMessages = Map.empty - static let mutable _chatRooms = Map.empty - static let mutable _organizations = - let newId = OrganizationId (Guid.Parse("51f823ef-2294-41dc-9f39-a4b9a237317a")) - ( newId, - { Organization_In_Db.Id = newId - Name = "Public" - Members = [] - ChatRooms = [] } - ) - |> List.singleton - |> Map.ofList - - static member Members - with get() = _members - and set(v) = _members <- v - - static member ChatMembers - with get() = _chatMembers - and set(v) = _chatMembers <- v - - static member ChatRoomMessages - with get() = _chatRoomMessages - and set(v) = _chatRoomMessages <- v - - static member ChatRooms - with get() = _chatRooms - and set(v) = _chatRooms <- v - - static member Organizations - with get() = _organizations - and set(v) = _organizations <- v - module MapFrom = let memberInDb_To_Member (x : Member_In_Db) : Member = { Id = x.Id From d3d737fb66f71b1540eac1a787f3d07bf8e9e372 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 16:18:40 +0100 Subject: [PATCH 024/100] chat-app sample: moved exception-related functions to own module According to suggestion during code review. --- samples/chat-app/server/Exceptions.fs | 26 ++++++++++++++ ...FSharp.Data.GraphQL.Samples.ChatApp.fsproj | 1 + samples/chat-app/server/Schema.fs | 34 ++++--------------- 3 files changed, 33 insertions(+), 28 deletions(-) create mode 100644 samples/chat-app/server/Exceptions.fs diff --git a/samples/chat-app/server/Exceptions.fs b/samples/chat-app/server/Exceptions.fs new file mode 100644 index 000000000..2fb614d9a --- /dev/null +++ b/samples/chat-app/server/Exceptions.fs @@ -0,0 +1,26 @@ +namespace FSharp.Data.GraphQL.Samples.ChatApp + +module Exceptions = + open FSharp.Data.GraphQL + + let Member_With_This_Name_Already_Exists (theName : string) = + GraphQLException(sprintf "member with name \"%s\" already exists" theName) + + let Organization_Doesnt_Exist (theId : OrganizationId) = + match theId with + | OrganizationId x -> + GraphQLException (sprintf "organization with ID \"%s\" doesn't exist" (x.ToString())) + + let ChatRoom_Doesnt_Exist (theId : ChatRoomId) = + GraphQLException(sprintf "chat room with ID \"%s\" doesn't exist" (theId.ToString())) + + let PrivMember_Doesnt_Exist (theId : MemberPrivateId) = + match theId with + | MemberPrivateId x -> + GraphQLException(sprintf "member with private ID \"%s\" doesn't exist" (x.ToString())) + + let Member_Isnt_Part_Of_Org () = + GraphQLException("this member is not part of this organization") + + let ChatRoom_Isnt_Part_Of_Org () = + GraphQLException("this chat room is not part of this organization") \ No newline at end of file diff --git a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj index b9b712f71..c7e2b9a29 100644 --- a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj +++ b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj @@ -7,6 +7,7 @@ + diff --git a/samples/chat-app/server/Schema.fs b/samples/chat-app/server/Schema.fs index 5f83a0541..313267ed6 100644 --- a/samples/chat-app/server/Schema.fs +++ b/samples/chat-app/server/Schema.fs @@ -78,50 +78,28 @@ module MapFrom = module Schema = open FSharp.Data.GraphQL.Server.AspNetCore.Rop - let validationException_Member_With_This_Name_Already_Exists (theName : string) = - GraphQLException(sprintf "member with name \"%s\" already exists" theName) - - let validationException_Organization_Doesnt_Exist (theId : OrganizationId) = - match theId with - | OrganizationId x -> - GraphQLException (sprintf "organization with ID \"%s\" doesn't exist" (x.ToString())) - - let validationException_ChatRoom_Doesnt_Exist (theId : ChatRoomId) = - GraphQLException(sprintf "chat room with ID \"%s\" doesn't exist" (theId.ToString())) - - let validationException_PrivMember_Doesnt_Exist (theId : MemberPrivateId) = - match theId with - | MemberPrivateId x -> - GraphQLException(sprintf "member with private ID \"%s\" doesn't exist" (x.ToString())) - - let validationException_Member_Isnt_Part_Of_Org () = - GraphQLException("this member is not part of this organization") - - let validationException_ChatRoom_Isnt_Part_Of_Org () = - GraphQLException("this chat room is not part of this organization") - let authenticateMemberInOrganization (organizationId : OrganizationId) (memberPrivId : MemberPrivateId) : RopResult<(Organization_In_Db * Member_In_Db), GraphQLException> = let maybeOrganization = FakePersistence.Organizations |> Map.tryFind organizationId let maybeMember = FakePersistence.Members.Values |> Seq.tryFind (fun x -> x.PrivId = memberPrivId) match (maybeOrganization, maybeMember) with | None, _ -> - fail <| (organizationId |> validationException_Organization_Doesnt_Exist) + fail <| (organizationId |> Exceptions.Organization_Doesnt_Exist) | _, None -> - fail <| (memberPrivId |> validationException_PrivMember_Doesnt_Exist) + fail <| (memberPrivId |> Exceptions.PrivMember_Doesnt_Exist) | Some organization, Some theMember -> if not (organization.Members |> List.contains theMember.Id) then - fail <| (validationException_Member_Isnt_Part_Of_Org()) + fail <| (Exceptions.Member_Isnt_Part_Of_Org()) else succeed (organization, theMember) let validateChatRoomExistence (organization : Organization_In_Db) (chatRoomId : ChatRoomId) : RopResult = match FakePersistence.ChatRooms |> Map.tryFind chatRoomId with | None -> - fail <| validationException_ChatRoom_Doesnt_Exist chatRoomId + fail <| Exceptions.ChatRoom_Doesnt_Exist chatRoomId | Some chatRoom -> if not (organization.ChatRooms |> List.contains chatRoom.Id) then - fail <| validationException_ChatRoom_Isnt_Part_Of_Org() + fail <| Exceptions.ChatRoom_Isnt_Part_Of_Org() else succeed <| chatRoom @@ -389,7 +367,7 @@ module Schema = |> Option.map (fun organization -> if organization.Members |> List.exists (fun m -> m.Name = newMemberName) then - raise (newMemberName |> validationException_Member_With_This_Name_Already_Exists) + raise (newMemberName |> Exceptions.Member_With_This_Name_Already_Exists) else let newMemberPrivId = MemberPrivateId (Guid.NewGuid()) let newMemberId = MemberId (Guid.NewGuid()) From f34167e83644e7de66d0fb10a10f9a08adacd268 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 16:25:51 +0100 Subject: [PATCH 025/100] Server.AspNetCore: removing now superfluous "executor" parameters The executor was needed for (de)serialization, but now it is already passed along in the JsonSerializerOptions. --- .../GraphQLWebsocketMiddleware.fs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 69c0c049e..923082d57 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -74,7 +74,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti not (theSocket.State = WebSocketState.Aborted) && not (theSocket.State = WebSocketState.Closed) - let receiveMessageViaSocket (cancellationToken : CancellationToken) (serializerOptions: JsonSerializerOptions) (executor : Executor<'Root>) (socket : WebSocket) : Task option> = + let receiveMessageViaSocket (cancellationToken : CancellationToken) (serializerOptions: JsonSerializerOptions) (socket : WebSocket) : Task option> = task { let buffer = Array.zeroCreate 4096 let completeMessage = new List() @@ -171,7 +171,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti let safe_Send = sendMessageViaSocket serializerOptions socket let safe_Receive() = socket - |> safe_ReceiveMessageViaSocket serializerOptions executor + |> safe_ReceiveMessageViaSocket serializerOptions let safe_SendQueryOutput id output = let outputAsDict = output :> IDictionary @@ -290,7 +290,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti // <-- Main // <-------- - let waitForConnectionInitAndRespondToClient (serializerOptions : JsonSerializerOptions) (schemaExecutor : Executor<'Root>) (connectionInitTimeoutInMs : int) (socket : WebSocket) : Task> = + let waitForConnectionInitAndRespondToClient (serializerOptions : JsonSerializerOptions) (connectionInitTimeoutInMs : int) (socket : WebSocket) : Task> = task { let timerTokenSource = new CancellationTokenSource() timerTokenSource.CancelAfter(connectionInitTimeoutInMs) @@ -302,7 +302,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti let! connectionInitSucceeded = Task.Run((fun _ -> task { logger.LogDebug("Waiting for ConnectionInit...") - let! receivedMessage = receiveMessageViaSocket (CancellationToken.None) serializerOptions schemaExecutor socket + let! receivedMessage = receiveMessageViaSocket (CancellationToken.None) serializerOptions socket match receivedMessage with | Some (Success (ConnectionInit payload, _)) -> logger.LogDebug("Valid connection_init received! Responding with ACK!") @@ -343,7 +343,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti use! socket = ctx.WebSockets.AcceptWebSocketAsync("graphql-transport-ws") let! connectionInitResult = socket - |> waitForConnectionInitAndRespondToClient options.SerializerOptions options.SchemaExecutor options.WebsocketOptions.ConnectionInitTimeoutInMs + |> waitForConnectionInitAndRespondToClient options.SerializerOptions options.WebsocketOptions.ConnectionInitTimeoutInMs match connectionInitResult with | Failure errMsg -> logger.LogWarning("{warningmsg}", (sprintf "%A" errMsg)) From 2d516b5c71c600c60a50385d66aab8f6ef918b2c Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 16:46:19 +0100 Subject: [PATCH 026/100] Server.AspNetCore: trying to improve readability by changing func sig. According to suggestion during code review. --- .../GraphQLWebsocketMiddleware.fs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 923082d57..5d54eaea0 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -121,7 +121,13 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti logger.LogTrace("<- Response: {response}", message) } - let addClientSubscription (id : SubscriptionId) (jsonSerializerOptions) (socket) (howToSendDataOnNext: SubscriptionId -> Output -> Task) (streamSource: IObservable) (subscriptions : SubscriptionsDict) = + let addClientSubscription + (id : SubscriptionId) + (howToSendDataOnNext: SubscriptionId -> Output -> Task) + ( subscriptions : SubscriptionsDict, + socket : WebSocket, + streamSource: IObservable, + jsonSerializerOptions : JsonSerializerOptions ) = let observer = new Reactive.AnonymousObserver( onNext = (fun theOutput -> @@ -196,14 +202,14 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti task { match executionResult with | Stream observableOutput -> - subscriptions - |> addClientSubscription id serializerOptions socket safe_SendQueryOutput observableOutput + (subscriptions, socket, observableOutput, serializerOptions) + |> addClientSubscription id safe_SendQueryOutput | Deferred (data, errors, observableOutput) -> do! data |> safe_SendQueryOutput id if errors.IsEmpty then - subscriptions - |> addClientSubscription id serializerOptions socket (safe_SendQueryOutputDelayedBy 5000) observableOutput + (subscriptions, socket, observableOutput, serializerOptions) + |> addClientSubscription id (safe_SendQueryOutputDelayedBy 5000) else () | Direct (data, _) -> From e138939932181f1478000e657ed4e625b60c7db8 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 17:00:39 +0100 Subject: [PATCH 027/100] Server.AspNetCore: removed the `safe_` prefix in order to avoid confus. According to comments during code review. --- .../GraphQLWebsocketMiddleware.fs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 5d54eaea0..973b87ac4 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -172,49 +172,49 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti // ----------> // Helpers --> // ----------> - let safe_ReceiveMessageViaSocket = receiveMessageViaSocket (CancellationToken.None) + let rcvMsgViaSocket = receiveMessageViaSocket (CancellationToken.None) - let safe_Send = sendMessageViaSocket serializerOptions socket - let safe_Receive() = + let sendMsg = sendMessageViaSocket serializerOptions socket + let rcv() = socket - |> safe_ReceiveMessageViaSocket serializerOptions + |> rcvMsgViaSocket serializerOptions - let safe_SendQueryOutput id output = + let sendOutput id output = let outputAsDict = output :> IDictionary match outputAsDict.TryGetValue("errors") with | true, theValue -> // The specification says: "This message terminates the operation and no further messages will be sent." subscriptions |> GraphQLSubscriptionsManagement.removeSubscription(id) - safe_Send (Error (id, unbox theValue)) + sendMsg (Error (id, unbox theValue)) | false, _ -> - safe_Send (Next (id, output)) + sendMsg (Next (id, output)) - let sendQueryOutputDelayedBy (cancToken: CancellationToken) (ms: int) id output = + let sendOutputDelayedBy (cancToken: CancellationToken) (ms: int) id output = task { do! Async.StartAsTask(Async.Sleep ms, cancellationToken = cancToken) do! output - |> safe_SendQueryOutput id + |> sendOutput id } - let safe_SendQueryOutputDelayedBy = sendQueryOutputDelayedBy cancellationToken + let sendQueryOutputDelayedBy = sendOutputDelayedBy cancellationToken - let safe_ApplyPlanExecutionResult (id: SubscriptionId) (socket) (executionResult: GQLResponse) = + let applyPlanExecutionResult (id: SubscriptionId) (socket) (executionResult: GQLResponse) = task { match executionResult with | Stream observableOutput -> (subscriptions, socket, observableOutput, serializerOptions) - |> addClientSubscription id safe_SendQueryOutput + |> addClientSubscription id sendOutput | Deferred (data, errors, observableOutput) -> do! data - |> safe_SendQueryOutput id + |> sendOutput id if errors.IsEmpty then (subscriptions, socket, observableOutput, serializerOptions) - |> addClientSubscription id (safe_SendQueryOutputDelayedBy 5000) + |> addClientSubscription id (sendQueryOutputDelayedBy 5000) else () | Direct (data, _) -> do! data - |> safe_SendQueryOutput id + |> sendOutput id } let getStrAddendumOfOptionalPayload optionalPayload = @@ -238,7 +238,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti task { try while not cancellationToken.IsCancellationRequested && socket |> isSocketOpen do - let! receivedMessage = safe_Receive() + let! receivedMessage = rcv() match receivedMessage with | None -> logger.LogTrace("Websocket socket received empty message! (socket state = {socketstate})", socket.State) @@ -260,9 +260,9 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti match pingHandler with | Some func -> let! customP = p |> func serviceProvider - do! ServerPong customP |> safe_Send + do! ServerPong customP |> sendMsg | None -> - do! ServerPong p |> safe_Send + do! ServerPong p |> sendMsg | Success (ClientPong p, _) -> "ClientPong" |> logMsgReceivedWithOptionalPayload p | Success (Subscribe (id, query), _) -> @@ -277,7 +277,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti executor.AsyncExecute(query.ExecutionPlan, root(), query.Variables) |> Async.StartAsTask do! planExecutionResult - |> safe_ApplyPlanExecutionResult id socket + |> applyPlanExecutionResult id socket | Success (ClientComplete id, _) -> "ClientComplete" |> logMsgWithIdReceived id subscriptions |> GraphQLSubscriptionsManagement.removeSubscription (id) From 89207d044bca2a61b04169c87d1be35c44600e84 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 17:04:47 +0100 Subject: [PATCH 028/100] Server.AspNetCore renaming tests to be more explicit about what's tested According to suggestion during code review. --- .../AspNetCore/InvalidMessageTests.fs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs index f21cf7f58..39a624309 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs @@ -35,7 +35,7 @@ let willResultInJsonException input = Assert.True(true) [] -let ``unknown message type`` () = +let ``Unknown message type will result in invalid message`` () = """{ "type": "connection_start" } @@ -43,7 +43,7 @@ let ``unknown message type`` () = |> willResultInInvalidMessage "invalid type \"connection_start\" specified by client." [] -let ``type not specified`` () = +let ``Type not specified will result in invalid message`` () = """{ "payload": "hello, let us connect" } @@ -51,7 +51,7 @@ let ``type not specified`` () = |> willResultInInvalidMessage "property \"type\" is missing" [] -let ``no payload in subscribe message`` () = +let ``No payload in subscribe message will result in invalid message`` () = """{ "type": "subscribe", "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6" @@ -60,7 +60,7 @@ let ``no payload in subscribe message`` () = |> willResultInInvalidMessage "payload is required for this message, but none was present." [] -let ``null payload json in subscribe message`` () = +let ``Null payload json in subscribe message will result in invalid message`` () = """{ "type": "subscribe", "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", @@ -70,7 +70,7 @@ let ``null payload json in subscribe message`` () = |> willResultInInvalidMessage "payload is required for this message, but none was present." [] -let ``payload type of number in subscribe message`` () = +let ``Payload type of number in subscribe message will result in invalid message`` () = """{ "type": "subscribe", "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", @@ -80,7 +80,7 @@ let ``payload type of number in subscribe message`` () = |> willResultInInvalidMessage "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AspNetCore.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 2." [] -let ``no id in subscribe message`` () = +let ``No id in subscribe message will result in invalid message`` () = """{ "type": "subscribe", "payload": { @@ -91,7 +91,7 @@ let ``no id in subscribe message`` () = |> willResultInInvalidMessage "property \"id\" is required for this message but was not present." [] -let ``string payload wrongly used in subscribe`` () = +let ``String payload wrongly used in subscribe will result in invalid message`` () = """{ "type": "subscribe", "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", @@ -101,7 +101,7 @@ let ``string payload wrongly used in subscribe`` () = |> willResultInInvalidMessage "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AspNetCore.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 79." [] -let ``id is incorrectly a number in a subscribe message`` () = +let ``Id is incorrectly a number in a subscribe message will result in JsonException`` () = """{ "type": "subscribe", "id": 42, @@ -113,7 +113,7 @@ let ``id is incorrectly a number in a subscribe message`` () = |> willResultInJsonException [] -let ``typo in one of the messages root properties`` () = +let ``Typo in one of the messages root properties will result in invalid message`` () = """{ "typo": "subscribe", "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", @@ -125,7 +125,7 @@ let ``typo in one of the messages root properties`` () = |> willResultInInvalidMessage "unknown property \"typo\"" [] -let ``complete message without an id`` () = +let ``Complete message without an id will result in invalid message`` () = """{ "type": "complete" } @@ -133,7 +133,7 @@ let ``complete message without an id`` () = |> willResultInInvalidMessage "property \"id\" is required for this message but was not present." [] -let ``complete message with a null id`` () = +let ``Complete message with a null id will result in invalid message`` () = """{ "type": "complete", "id": null From 9f8cc2ed15b45f9539101df3bb504ba64089fc9d Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 17:20:55 +0100 Subject: [PATCH 029/100] added Server.AspnetCore to github workflows --- .github/workflows/publish_ci.yml | 8 ++++++++ .github/workflows/publish_release.yml | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/.github/workflows/publish_ci.yml b/.github/workflows/publish_ci.yml index b2220aadd..d5efdd31a 100644 --- a/.github/workflows/publish_ci.yml +++ b/.github/workflows/publish_ci.yml @@ -61,6 +61,14 @@ jobs: run: | dotnet nuget push nuget/*Server*.nupkg -s "github" -k ${{secrets.GITHUB_TOKEN}} + - name: Pack FSharp.Data.GraphQL.Server.AspNetCore project + run: | + cd src/FSharp.Data.GraphQL.Server.AspNetCore + dotnet pack --no-build --nologo --configuration Release /p:IsNuget=true -o ../../nuget + - name: Publish FSharp.Data.GraphQL.Server.AspNetCore project to GitHub + run: | + dotnet nuget push nuget/*Server.AspNetCore*.nupkg -s "github" -k ${{secrets.GITHUB_TOKEN}} + - name: Pack FSharp.Data.GraphQL.Server.Relay project run: | cd src/FSharp.Data.GraphQL.Server.Relay diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 560ccbd77..f4f2719f1 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -60,6 +60,14 @@ jobs: run: | dotnet nuget push nuget/*Server*.nupkg -k ${{secrets.NUGET_SECRET}} + - name: Pack FSharp.Data.GraphQL.Server.AspNetCore project + run: | + cd src/FSharp.Data.GraphQL.Server.AspNetCore + dotnet pack --no-build --nologo --configuration Release /p:IsNuget=true -o ../../nuget + - name: Publish FSharp.Data.GraphQL.Server.AspNetCore project to NuGet + run: | + dotnet nuget push nuget/*Server.AspNetCore*.nupkg -k ${{secrets.NUGET_SECRET}} + - name: Pack FSharp.Data.GraphQL.Server.Relay project run: | cd src/FSharp.Data.GraphQL.Server.Relay From a79088235dcf55742f516aac202e738da2ea085f Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 17:30:47 +0100 Subject: [PATCH 030/100] Server.AspNetCore: trying to simplify module declaration According to suggestion during code review. --- .../GraphQLSubscriptionsManagement.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs index d2d589d86..5dde51652 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs @@ -1,6 +1,5 @@ -namespace FSharp.Data.GraphQL.Server.AspNetCore +module internal FSharp.Data.GraphQL.Server.AspNetCore.GraphQLSubscriptionsManagement -module internal GraphQLSubscriptionsManagement = let addSubscription (id : SubscriptionId, unsubscriber : SubscriptionUnsubscriber, onUnsubscribe : OnUnsubscribeAction) (subscriptions : SubscriptionsDict) = subscriptions.Add(id, (unsubscriber, onUnsubscribe)) From b66a0f6f12c4f2e7bb9e689ce5ab924cd589a752 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 17:41:22 +0100 Subject: [PATCH 031/100] Server.AspNetCore: complement to my last commit (identation) Fixing identation. According to suggestion during code review. --- .../GraphQLSubscriptionsManagement.fs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs index 5dde51652..70734023c 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs @@ -1,31 +1,31 @@ module internal FSharp.Data.GraphQL.Server.AspNetCore.GraphQLSubscriptionsManagement - let addSubscription (id : SubscriptionId, unsubscriber : SubscriptionUnsubscriber, onUnsubscribe : OnUnsubscribeAction) - (subscriptions : SubscriptionsDict) = - subscriptions.Add(id, (unsubscriber, onUnsubscribe)) +let addSubscription (id : SubscriptionId, unsubscriber : SubscriptionUnsubscriber, onUnsubscribe : OnUnsubscribeAction) + (subscriptions : SubscriptionsDict) = + subscriptions.Add(id, (unsubscriber, onUnsubscribe)) - let isIdTaken (id : SubscriptionId) (subscriptions : SubscriptionsDict) = - subscriptions.ContainsKey(id) +let isIdTaken (id : SubscriptionId) (subscriptions : SubscriptionsDict) = + subscriptions.ContainsKey(id) - let executeOnUnsubscribeAndDispose (id : SubscriptionId) (subscription : SubscriptionUnsubscriber * OnUnsubscribeAction) = - match subscription with - | unsubscriber, onUnsubscribe -> - try - id |> onUnsubscribe - finally - unsubscriber.Dispose() +let executeOnUnsubscribeAndDispose (id : SubscriptionId) (subscription : SubscriptionUnsubscriber * OnUnsubscribeAction) = + match subscription with + | unsubscriber, onUnsubscribe -> + try + id |> onUnsubscribe + finally + unsubscriber.Dispose() - let removeSubscription (id: SubscriptionId) (subscriptions : SubscriptionsDict) = - if subscriptions.ContainsKey(id) then - subscriptions.[id] - |> executeOnUnsubscribeAndDispose id - subscriptions.Remove(id) |> ignore +let removeSubscription (id: SubscriptionId) (subscriptions : SubscriptionsDict) = + if subscriptions.ContainsKey(id) then + subscriptions.[id] + |> executeOnUnsubscribeAndDispose id + subscriptions.Remove(id) |> ignore - let removeAllSubscriptions (subscriptions : SubscriptionsDict) = - subscriptions - |> Seq.iter - (fun subscription -> - subscription.Value - |> executeOnUnsubscribeAndDispose subscription.Key - ) - subscriptions.Clear() \ No newline at end of file +let removeAllSubscriptions (subscriptions : SubscriptionsDict) = + subscriptions + |> Seq.iter + (fun subscription -> + subscription.Value + |> executeOnUnsubscribeAndDispose subscription.Key + ) + subscriptions.Clear() \ No newline at end of file From 4b2937a5cf35d1100e30092e1903582871cf4f10 Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sun, 19 Mar 2023 17:44:37 +0100 Subject: [PATCH 032/100] Update samples/chat-app/server/Exceptions.fs Summarizing module + Adjusting identation Co-authored-by: Andrii Chebukin --- samples/chat-app/server/Exceptions.fs | 37 +++++++++++++-------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/samples/chat-app/server/Exceptions.fs b/samples/chat-app/server/Exceptions.fs index 2fb614d9a..54c9217eb 100644 --- a/samples/chat-app/server/Exceptions.fs +++ b/samples/chat-app/server/Exceptions.fs @@ -1,26 +1,25 @@ -namespace FSharp.Data.GraphQL.Samples.ChatApp +module FSharp.Data.GraphQL.Samples.ChatApp.Exceptions -module Exceptions = - open FSharp.Data.GraphQL +open FSharp.Data.GraphQL - let Member_With_This_Name_Already_Exists (theName : string) = - GraphQLException(sprintf "member with name \"%s\" already exists" theName) +let Member_With_This_Name_Already_Exists (theName : string) = + GraphQLException(sprintf "member with name \"%s\" already exists" theName) - let Organization_Doesnt_Exist (theId : OrganizationId) = - match theId with - | OrganizationId x -> - GraphQLException (sprintf "organization with ID \"%s\" doesn't exist" (x.ToString())) +let Organization_Doesnt_Exist (theId : OrganizationId) = + match theId with + | OrganizationId x -> + GraphQLException (sprintf "organization with ID \"%s\" doesn't exist" (x.ToString())) - let ChatRoom_Doesnt_Exist (theId : ChatRoomId) = - GraphQLException(sprintf "chat room with ID \"%s\" doesn't exist" (theId.ToString())) +let ChatRoom_Doesnt_Exist (theId : ChatRoomId) = + GraphQLException(sprintf "chat room with ID \"%s\" doesn't exist" (theId.ToString())) - let PrivMember_Doesnt_Exist (theId : MemberPrivateId) = - match theId with - | MemberPrivateId x -> - GraphQLException(sprintf "member with private ID \"%s\" doesn't exist" (x.ToString())) +let PrivMember_Doesnt_Exist (theId : MemberPrivateId) = + match theId with + | MemberPrivateId x -> + GraphQLException(sprintf "member with private ID \"%s\" doesn't exist" (x.ToString())) - let Member_Isnt_Part_Of_Org () = - GraphQLException("this member is not part of this organization") +let Member_Isnt_Part_Of_Org () = + GraphQLException("this member is not part of this organization") - let ChatRoom_Isnt_Part_Of_Org () = - GraphQLException("this chat room is not part of this organization") \ No newline at end of file +let ChatRoom_Isnt_Part_Of_Org () = + GraphQLException("this chat room is not part of this organization") \ No newline at end of file From 619bae2b878d9ae0a28afc7ab15634ca6cea70c9 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 17:52:56 +0100 Subject: [PATCH 033/100] Authors += ", valbers" --- Directory.Build.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 2490d55f1..5265f6cff 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -10,7 +10,7 @@ - John Bazinga, Andrii Chebukin, Jurii Chebukin, Ismael Carlos Velten, njlr + John Bazinga, Andrii Chebukin, Jurii Chebukin, Ismael Carlos Velten, njlr, valbers FSharp.Data.GraphQL F# implementation of Facebook GraphQL query language From fefe03b418c8b04d2321ac1843cfaee5d22981b7 Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sun, 19 Mar 2023 18:02:51 +0100 Subject: [PATCH 034/100] Using Task.WhenAll Co-authored-by: Andrii Chebukin --- .../GraphQLWebsocketMiddleware.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 973b87ac4..e1258644a 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -359,8 +359,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti longRunningCancellationToken.Register(fun _ -> socket |> tryToGracefullyCloseSocketWithDefaultBehavior - |> Async.AwaitTask - |> Async.RunSynchronously + |> Task.WhenAll ) |> ignore let safe_HandleMessages = handleMessages longRunningCancellationToken try From 4d2a8e59632f34befe8e290bc5b053babcbd6afa Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sun, 19 Mar 2023 18:03:52 +0100 Subject: [PATCH 035/100] Added TODO comment related to string allocation Co-authored-by: Andrii Chebukin --- .../GraphQLWebsocketMiddleware.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index e1258644a..338773a92 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -89,6 +89,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti with :? OperationCanceledException -> () + // TODO: Allocate string only if a debugger is attached let message = completeMessage |> Seq.filter (fun x -> x > 0uy) From ce386c83a28fe4baefd5e78d455d0a8c47f52a8e Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sun, 19 Mar 2023 18:04:21 +0100 Subject: [PATCH 036/100] Added TODO comment regarding string allocation Co-authored-by: Andrii Chebukin --- .../GraphQLWebsocketMiddleware.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 338773a92..5cb6868ea 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -109,6 +109,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti if not (socket.State = WebSocketState.Open) then logger.LogTrace("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) else + // TODO: Allocate string only if a debugger is attached let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions let segment = new ArraySegment( From 996cfe5f7bcef3e8343afcb4e5c87b9e73a842b3 Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sun, 19 Mar 2023 18:04:53 +0100 Subject: [PATCH 037/100] Using better suited logger.LogError overload Co-authored-by: Andrii Chebukin --- .../GraphQLWebsocketMiddleware.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 5cb6868ea..e8e5f80b6 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -139,7 +139,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti ), onError = (fun ex -> - logger.LogError("[Error on subscription {id}]: {exceptionstr}", id, ex) + logger.LogError(ex, "Error on subscription with id='{id}'", id) ), onCompleted = (fun () -> From 447af9a44c0d6b6a48971e58fb3b415d55f81458 Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sun, 19 Mar 2023 18:06:32 +0100 Subject: [PATCH 038/100] Using Task.WhenAll again (instead of Async.AwaitTask + Async.RunSynchr...) Co-authored-by: Andrii Chebukin --- .../GraphQLWebsocketMiddleware.fs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index e8e5f80b6..2910aab65 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -144,9 +144,8 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti onCompleted = (fun () -> Complete id - |> sendMessageViaSocket jsonSerializerOptions (socket) - |> Async.AwaitTask - |> Async.RunSynchronously + |> sendMessageViaSocket jsonSerializerOptions socket + |> Task.WhenAll subscriptions |> GraphQLSubscriptionsManagement.removeSubscription(id) ) From 7725b7bc6030067c49f9a691b5b4f25e252ebe53 Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Sun, 19 Mar 2023 18:07:14 +0100 Subject: [PATCH 039/100] Making code better readable Co-authored-by: Andrii Chebukin --- .../GraphQLWebsocketMiddleware.fs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 2910aab65..a270e73cb 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -157,13 +157,11 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti |> GraphQLSubscriptionsManagement.addSubscription(id, unsubscriber, (fun _ -> ())) let tryToGracefullyCloseSocket (code, message) theSocket = - task { - if theSocket |> canCloseSocket - then - do! theSocket.CloseAsync(code, message, CancellationToken.None) - else - () - } + if theSocket |> canCloseSocket + then + theSocket.CloseAsync(code, message, CancellationToken.None) + else + Task.CompletedTask let tryToGracefullyCloseSocketWithDefaultBehavior = tryToGracefullyCloseSocket (WebSocketCloseStatus.NormalClosure, "Normal Closure") From a05eb4370a46c6a92c8f9f44faf564c70460f21d Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 18:12:32 +0100 Subject: [PATCH 040/100] Revert "Using Task.WhenAll again (instead of Async.AwaitTask + Async.RunSynchr...)" This reverts commit 447af9a44c0d6b6a48971e58fb3b415d55f81458. --- .../GraphQLWebsocketMiddleware.fs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index a270e73cb..514f53402 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -144,8 +144,9 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti onCompleted = (fun () -> Complete id - |> sendMessageViaSocket jsonSerializerOptions socket - |> Task.WhenAll + |> sendMessageViaSocket jsonSerializerOptions (socket) + |> Async.AwaitTask + |> Async.RunSynchronously subscriptions |> GraphQLSubscriptionsManagement.removeSubscription(id) ) From a1ef59ef12d40c0fcc8c76f6d80d94aeccb6e3b9 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 19 Mar 2023 18:13:07 +0100 Subject: [PATCH 041/100] Revert "Using Task.WhenAll" This reverts commit fefe03b418c8b04d2321ac1843cfaee5d22981b7. --- .../GraphQLWebsocketMiddleware.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 514f53402..418cf4679 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -359,7 +359,8 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti longRunningCancellationToken.Register(fun _ -> socket |> tryToGracefullyCloseSocketWithDefaultBehavior - |> Task.WhenAll + |> Async.AwaitTask + |> Async.RunSynchronously ) |> ignore let safe_HandleMessages = handleMessages longRunningCancellationToken try From 28c386a578ffc451e64eba203e751d048fab900c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 9 Feb 2024 18:31:12 +0400 Subject: [PATCH 042/100] Extracted the target frameworks into a single file --- Directory.Build.props | 42 +++++++++++++++++++ Directory.Build.targets | 39 ----------------- FSharp.Data.GraphQL.sln | 30 +------------ ...harp.Data.GraphQL.Server.Middleware.fsproj | 2 +- .../FSharp.Data.GraphQL.Server.Relay.fsproj | 2 +- .../FSharp.Data.GraphQL.Server.fsproj | 2 +- .../FSharp.Data.GraphQL.Shared.fsproj | 2 +- 7 files changed, 47 insertions(+), 72 deletions(-) create mode 100644 Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..5087e944d --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,42 @@ + + + + net6.0;net7.0 + 7.0 + 7.0.* + true + true + $(NoWarn);NU1504;NU1701 + true + + + + John Bazinga, Andrii Chebukin, Jurii Chebukin, Ismael Carlos Velten, njlr, Garrett Birkel + FSharp.Data.GraphQL + F# implementation of Facebook GraphQL query language + + $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) + https://github.com/fsprojects/FSharp.Data.GraphQL + git + 2.0.0 + FSharp GraphQL Relay React Middleware + README.md + icon.png + + https://fsprojects.github.io/FSharp.Data.GraphQL + false + MIT + true + true + snupkg + true + + v + + + + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets index a5721141c..3b6a67815 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,42 +1,3 @@ - - - 7.0 - 7.0.* - true - true - $(NoWarn);NU1504;NU1701 - true - - - - John Bazinga, Andrii Chebukin, Jurii Chebukin, Ismael Carlos Velten, njlr, Garrett Birkel - FSharp.Data.GraphQL - F# implementation of Facebook GraphQL query language - - $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) - https://github.com/fsprojects/FSharp.Data.GraphQL - git - 2.0.0 - FSharp GraphQL Relay React Middleware - README.md - icon.png - - https://fsprojects.github.io/FSharp.Data.GraphQL - false - MIT - true - true - snupkg - true - - v - - - - - - - diff --git a/FSharp.Data.GraphQL.sln b/FSharp.Data.GraphQL.sln index 76c80a12e..c8e60669f 100644 --- a/FSharp.Data.GraphQL.sln +++ b/FSharp.Data.GraphQL.sln @@ -6,6 +6,7 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E3330910-8B6C-4191-8046-D6D57FBC39B1}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets global.json = global.json LICENSE.txt = LICENSE.txt @@ -261,30 +262,6 @@ Global {F7858DA7-E067-486B-9E9C-697F0A56C620}.Release|x64.Build.0 = Release|Any CPU {F7858DA7-E067-486B-9E9C-697F0A56C620}.Release|x86.ActiveCfg = Release|Any CPU {F7858DA7-E067-486B-9E9C-697F0A56C620}.Release|x86.Build.0 = Release|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Debug|x64.ActiveCfg = Debug|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Debug|x64.Build.0 = Debug|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Debug|x86.ActiveCfg = Debug|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Debug|x86.Build.0 = Debug|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Release|Any CPU.Build.0 = Release|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Release|x64.ActiveCfg = Release|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Release|x64.Build.0 = Release|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Release|x86.ActiveCfg = Release|Any CPU - {B837B3ED-83CE-446F-A4E5-44CB06AA6505}.Release|x86.Build.0 = Release|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Debug|x64.ActiveCfg = Debug|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Debug|x64.Build.0 = Debug|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Debug|x86.ActiveCfg = Debug|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Debug|x86.Build.0 = Debug|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Release|Any CPU.Build.0 = Release|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Release|x64.ActiveCfg = Release|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Release|x64.Build.0 = Release|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Release|x86.ActiveCfg = Release|Any CPU - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE}.Release|x86.Build.0 = Release|Any CPU {54AAFE43-FA5F-485A-AD40-0240165FC633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54AAFE43-FA5F-485A-AD40-0240165FC633}.Debug|Any CPU.Build.0 = Debug|Any CPU {54AAFE43-FA5F-485A-AD40-0240165FC633}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -330,11 +307,6 @@ Global {A6A162DF-9FBB-4C2A-913F-FD5FED35A09B} = {ED8079DD-2B06-4030-9F0F-DC548F98E1C4} {CAE5916B-1415-4982-B705-7318D77C029C} = {B0C25450-74BF-40C2-9E02-09AADBAE2C2F} {3D948D55-3CD2-496D-A04C-3B4E7BB69140} = {B0C25450-74BF-40C2-9E02-09AADBAE2C2F} - {6768EA38-1335-4B8E-BC09-CCDED1F9AAF6} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78} - {8FB23F61-77CB-42C7-8EEC-B22D7C4E4067} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78} - {A6A162DF-9FBB-4C2A-913F-FD5FED35A09B} = {ED8079DD-2B06-4030-9F0F-DC548F98E1C4} - {CAE5916B-1415-4982-B705-7318D77C029C} = {B0C25450-74BF-40C2-9E02-09AADBAE2C2F} - {3D948D55-3CD2-496D-A04C-3B4E7BB69140} = {B0C25450-74BF-40C2-9E02-09AADBAE2C2F} {A47968E2-CDD1-4BCF-9093-D0C5225A815B} = {3D948D55-3CD2-496D-A04C-3B4E7BB69140} {9D5C46E8-0C07-4384-8E58-903F7C2C7171} = {A47968E2-CDD1-4BCF-9093-D0C5225A815B} {600D4BE2-FCE0-4684-AC6F-2DC829B395BA} = {B0C25450-74BF-40C2-9E02-09AADBAE2C2F} 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 36cba278a..fbb9f5592 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 @@ -1,7 +1,7 @@  - net6.0;net7.0 + $(PackageTargetFrameworks) false true true diff --git a/src/FSharp.Data.GraphQL.Server.Relay/FSharp.Data.GraphQL.Server.Relay.fsproj b/src/FSharp.Data.GraphQL.Server.Relay/FSharp.Data.GraphQL.Server.Relay.fsproj index aca78cc36..d1b4c948d 100644 --- a/src/FSharp.Data.GraphQL.Server.Relay/FSharp.Data.GraphQL.Server.Relay.fsproj +++ b/src/FSharp.Data.GraphQL.Server.Relay/FSharp.Data.GraphQL.Server.Relay.fsproj @@ -1,7 +1,7 @@  - net6.0;net7.0 + $(PackageTargetFrameworks) true true true 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 a9537a002..29355400e 100644 --- a/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj +++ b/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj @@ -1,7 +1,7 @@  - net6.0;net7.0 + $(PackageTargetFrameworks) true true true 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 de95a1b6d..df6976e5d 100644 --- a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj +++ b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj @@ -1,7 +1,7 @@  - netstandard2.0;net6.0;net7.0 + netstandard2.0;$(PackageTargetFrameworks) true true true From 7128a687fcbedbbaedc0ea66db7c755a7cc372ec Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 9 Feb 2024 19:23:10 +0400 Subject: [PATCH 043/100] Suppressed the warning about the `WebClient` in the `Build` project --- build/NuGet.fs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/NuGet.fs b/build/NuGet.fs index 0e9f5d8dd..409c2c652 100644 --- a/build/NuGet.fs +++ b/build/NuGet.fs @@ -1,5 +1,7 @@ namespace Fake.DotNet.NuGet +#nowarn "0044" + /// /// Contains helper functions and task which allow to inspect, create and publish /// NuGet packages. From 83f17348f13c0c577b5d6127a6710c645e115dc7 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 19 Oct 2023 02:56:45 +0400 Subject: [PATCH 044/100] Added constants for F# optional type names --- src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs | 7 +++++-- src/FSharp.Data.GraphQL.Server/Values.fs | 12 ++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs b/src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs index 67c8ab723..f304e02ac 100644 --- a/src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs +++ b/src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs @@ -130,10 +130,13 @@ module internal Gen = module internal ReflectionHelper = + let [] OptionTypeName = "Microsoft.FSharp.Core.FSharpOption`1" + let [] ValueOptionTypeName = "Microsoft.FSharp.Core.FSharpValueOption`1" + let isParameterOptional (p: ParameterInfo) = p.IsOptional - || p.ParameterType.FullName.StartsWith "Microsoft.FSharp.Core.FSharpOption`1" - || p.ParameterType.FullName.StartsWith "Microsoft.FSharp.Core.FSharpValueOption`1" + || p.ParameterType.FullName.StartsWith OptionTypeName + || p.ParameterType.FullName.StartsWith ValueOptionTypeName let isPrameterMandatory = not << isParameterOptional diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index a30a68441..4e1f9b48d 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -20,7 +20,7 @@ open FSharp.Data.GraphQL let private wrapOptionalNone (outputType: Type) (inputType: Type) = if inputType.Name <> outputType.Name then - if outputType.FullName.StartsWith "Microsoft.FSharp.Core.FSharpValueOption`1" then + if outputType.FullName.StartsWith ReflectionHelper.ValueOptionTypeName then let _, valuenone, _ = ReflectionHelper.vOptionOfType outputType.GenericTypeArguments[0] valuenone elif outputType.IsValueType then @@ -35,12 +35,12 @@ let private wrapOptional (outputType: Type) value= | value -> let inputType = value.GetType() if inputType.Name <> outputType.Name then - let expectedType = outputType.GenericTypeArguments[0] - if outputType.FullName.StartsWith "Microsoft.FSharp.Core.FSharpOption`1" && expectedType.IsAssignableFrom inputType then - let some, _, _ = ReflectionHelper.optionOfType outputType.GenericTypeArguments[0] + let expectedOutputType = outputType.GenericTypeArguments[0] + if outputType.FullName.StartsWith ReflectionHelper.OptionTypeName && expectedOutputType.IsAssignableFrom inputType then + let some, _, _ = ReflectionHelper.optionOfType expectedOutputType some value - elif outputType.FullName.StartsWith "Microsoft.FSharp.Core.FSharpValueOption`1" && expectedType.IsAssignableFrom inputType then - let valuesome, _, _ = ReflectionHelper.vOptionOfType outputType.GenericTypeArguments[0] + elif outputType.FullName.StartsWith ReflectionHelper.ValueOptionTypeName && expectedOutputType.IsAssignableFrom inputType then + let valuesome, _, _ = ReflectionHelper.vOptionOfType expectedOutputType valuesome value else value From b37b99380c34908d85cbc864671174ab18bf34e2 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 19 Oct 2023 02:57:12 +0400 Subject: [PATCH 045/100] Fixed wrong condition on checking if dictionary is mutable --- src/FSharp.Data.GraphQL.Shared/Errors.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Shared/Errors.fs b/src/FSharp.Data.GraphQL.Shared/Errors.fs index 3d1613b54..a408f1d85 100644 --- a/src/FSharp.Data.GraphQL.Shared/Errors.fs +++ b/src/FSharp.Data.GraphQL.Shared/Errors.fs @@ -112,7 +112,7 @@ type GQLProblemDetails = { static member SetErrorKind (errorKind : ErrorKind) (extensions : IReadOnlyDictionary) = let mutableExtensions = match extensions with - | :? IDictionary as extensions -> extensions + | :? IDictionary as extensions when not extensions.IsReadOnly -> extensions | _ -> #if NETSTANDARD2_0 let dictionary = Dictionary (extensions.Count) From bb6c21e04e0302796f16764d59ff8a01ceb26357 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 19 Oct 2023 03:02:35 +0400 Subject: [PATCH 046/100] Implemented converting `Option` and `ValueOption` returned from scalar to the underlying type --- src/FSharp.Data.GraphQL.Server/Values.fs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index 4e1f9b48d..395929672 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -29,7 +29,7 @@ let private wrapOptionalNone (outputType: Type) (inputType: Type) = else null -let private wrapOptional (outputType: Type) value= +let private normalizeOptional (outputType: Type) value= match value with | null -> wrapOptionalNone outputType typeof | value -> @@ -43,7 +43,17 @@ let private wrapOptional (outputType: Type) value= let valuesome, _, _ = ReflectionHelper.vOptionOfType expectedOutputType valuesome value else - value + let realInputType = inputType.GenericTypeArguments[0] + if inputType.FullName.StartsWith ReflectionHelper.OptionTypeName && outputType.IsAssignableFrom realInputType then + let _, _, getValue = ReflectionHelper.optionOfType realInputType + // none is null so it is already covered above + getValue value + elif inputType.FullName.StartsWith ReflectionHelper.ValueOptionTypeName && outputType.IsAssignableFrom realInputType then + let _, valueNone, getValue = ReflectionHelper.vOptionOfType realInputType + if value = valueNone then null + else getValue value + else + value else value @@ -154,7 +164,7 @@ let rec internal compileByType (inputObjectPath: FieldPath) (inputSource : Input match Map.tryFind field.Name props with | None -> Ok <| wrapOptionalNone param.ParameterType field.TypeDef.Type | Some prop -> - field.ExecuteInput prop variables |> Result.map (wrapOptional param.ParameterType) + field.ExecuteInput prop variables |> Result.map (normalizeOptional param.ParameterType) |> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field | ValueNone -> Ok <| wrapOptionalNone param.ParameterType typeof) @@ -181,7 +191,7 @@ let rec internal compileByType (inputObjectPath: FieldPath) (inputSource : Input field.ExecuteInput (VariableName field.Name) objectFields // TODO: Take into account variable name |> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field - return wrapOptional param.ParameterType value + return normalizeOptional param.ParameterType value | ValueNone -> return wrapOptionalNone param.ParameterType typeof }) @@ -220,7 +230,7 @@ let rec internal compileByType (inputObjectPath: FieldPath) (inputSource : Input |> splitSeqErrorsList let mappedValues = mappedValues - |> Seq.map (wrapOptional innerDef.Type) + |> Seq.map (normalizeOptional innerDef.Type) |> Seq.toList if isArray then From b1e180b68196033b751c6c11e5a32c928426b52e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 19 Oct 2023 03:04:05 +0400 Subject: [PATCH 047/100] Implemented check that ValueOption returned from scalar coercion is allowed --- src/FSharp.Data.GraphQL.Server/Values.fs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index 395929672..693b71735 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -322,6 +322,16 @@ let rec internal coerceVariableValue | Ok null when isNullable -> Ok null // TODO: Capture position in the JSON document | Ok null -> createNullError originalTypeDef + | Ok value when not isNullable -> + let ``type`` = value.GetType() + if + ``type``.IsValueType && + ``type``.FullName.StartsWith ReflectionHelper.ValueOptionTypeName && + value = Activator.CreateInstance ``type`` + then + createNullError originalTypeDef + else + Ok value | result -> result |> Result.mapError (List.map (mapInputError varDef inputObjectPath objectFieldErrorDetails)) | Nullable (InputObject innerdef) -> From fe8712c2b120bbe2bf64c158a78c356db7fc5dc2 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sat, 10 Feb 2024 20:56:30 +0400 Subject: [PATCH 048/100] Added value object tests --- Packages.props | 1 + .../FSharp.Data.GraphQL.Tests/ErrorHelpers.fs | 10 +- .../FSharp.Data.GraphQL.Tests.fsproj | 3 + tests/FSharp.Data.GraphQL.Tests/Helpers.fs | 6 +- .../Variables and Inputs/CoercionTests.fs | 2 +- ...OptionalsNormalizationTests.ValidString.fs | 165 ++++++++++++ .../OptionalsNormalizationTests.fs | 244 ++++++++++++++++++ 7 files changed, 422 insertions(+), 9 deletions(-) create mode 100644 tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.ValidString.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs diff --git a/Packages.props b/Packages.props index 673f3b7a7..f37d360ad 100644 --- a/Packages.props +++ b/Packages.props @@ -73,5 +73,6 @@ + diff --git a/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs b/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs index 138b32bdf..2feb909ff 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs @@ -26,7 +26,7 @@ let ensureRequestError (result : GQLExecutionResult) (onRequestError : GQLProble | RequestError errors -> onRequestError errors | response -> fail $"Expected RequestError GQLResponse but got {Environment.NewLine}{response}" -let ensureValidationError (message : string) (path : FieldPath) (error) = +let ensureValidationError (message : string) (path : FieldPath) (error : GQLProblemDetails) = equals message error.Message equals (Include path) error.Path match error.Extensions with @@ -34,7 +34,7 @@ let ensureValidationError (message : string) (path : FieldPath) (error) = | Include extensions -> equals Validation (unbox extensions[CustomErrorFields.Kind]) -let ensureExecutionError (message : string) (path : FieldPath) (error) = +let ensureExecutionError (message : string) (path : FieldPath) (error : GQLProblemDetails) = equals message error.Message equals (Include path) error.Path match error.Extensions with @@ -42,7 +42,7 @@ let ensureExecutionError (message : string) (path : FieldPath) (error) = | Include extensions -> equals Execution (unbox extensions[CustomErrorFields.Kind]) -let ensureInputCoercionError (errorSource : ErrorSource) (message : string) (``type`` : string) (error) = +let ensureInputCoercionError (errorSource : ErrorSource) (message : string) (``type`` : string) (error : GQLProblemDetails) = equals message error.Message match error.Extensions with | Skip -> fail "Expected extensions to be present" @@ -56,7 +56,7 @@ let ensureInputCoercionError (errorSource : ErrorSource) (message : string) (``t equals name (unbox extensions[CustomErrorFields.ArgumentName]) equals ``type`` (unbox extensions[CustomErrorFields.ArgumentType]) -let ensureInputObjectFieldCoercionError (errorSource : ErrorSource) (message : string) (inputObjectPath : FieldPath) (objectType : string) (fieldType : string) (error) = +let ensureInputObjectFieldCoercionError (errorSource : ErrorSource) (message : string) (inputObjectPath : FieldPath) (objectType : string) (fieldType : string) (error : GQLProblemDetails) = equals message error.Message match error.Extensions with | Skip -> fail "Expected extensions to be present" @@ -70,7 +70,7 @@ let ensureInputObjectFieldCoercionError (errorSource : ErrorSource) (message : s equals objectType (unbox extensions[CustomErrorFields.ObjectType]) equals fieldType (unbox extensions[CustomErrorFields.FieldType]) -let ensureInputObjectValidationError (errorSource : ErrorSource) (message : string) (inputObjectPath : FieldPath) (objectType : string) (error) = +let ensureInputObjectValidationError (errorSource : ErrorSource) (message : string) (inputObjectPath : FieldPath) (objectType : string) (error : GQLProblemDetails) = equals message error.Message match error.Extensions with | Skip -> fail "Expected extensions to be present" 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 3c38c2014..8abaab60a 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -17,6 +17,7 @@ + @@ -51,6 +52,8 @@ + + diff --git a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs index 6cc0efa22..a50199789 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs @@ -89,13 +89,13 @@ let asts query = ["defer"; "stream"] |> Seq.map (query >> parse) -let set (mre : ManualResetEvent) = +let setEvent (mre : ManualResetEvent) = mre.Set() |> ignore -let reset (mre : ManualResetEvent) = +let resetEvent (mre : ManualResetEvent) = mre.Reset() |> ignore -let wait (mre : ManualResetEvent) errorMsg = +let waitEvent (mre : ManualResetEvent) errorMsg = if TimeSpan.FromSeconds(float 30) |> mre.WaitOne |> not then fail errorMsg diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/CoercionTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/CoercionTests.fs index c1282e101..af901f1bc 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/CoercionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/CoercionTests.fs @@ -1,5 +1,5 @@ // The MIT License (MIT) -/// Copyright (c) 2015-Mar 2016 Kevin Thompson @kthompson +// Copyright (c) 2015-Mar 2016 Kevin Thompson @kthompson // Copyright (c) 2016 Bazinga Technologies Inc [] diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.ValidString.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.ValidString.fs new file mode 100644 index 000000000..1bede223a --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.ValidString.fs @@ -0,0 +1,165 @@ +// The MIT License (MIT) + +namespace FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests + +open System +open FSharp.Data.GraphQL + +[] +type ValidString<'t> = internal ValidString of string +with + static member internal CreateVOption<'t> (value: string option) : ValidString<'t> voption = + value |> ValueOption.ofOption |> ValueOption.map ValidString + +module ValidString = + + let value (ValidString text) = text + + let vOption strOption : string voption = strOption |> ValueOption.map value + + let vOptionValue strOption : string = strOption |> ValueOption.map value |> ValueOption.toObj + +open Validus +open Validus.Operators + +module String = + + let notStartsWithWhiteSpace fieldName (s: string) = + if s.StartsWith ' ' then + Error + <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot start with whitespace" ] + else + Ok <| s + + let notEndsWithWhiteSpace fieldName (s: string) = + if s.EndsWith ' ' then + Error + <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot end with whitespace" ] + else + Ok <| s + + let notContainsWhiteSpace fieldName (s: string) = + if s.Contains ' ' then + Error + <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot contain whitespace" ] + else + Ok <| s + + let notContainsBacktick fieldName (s: string) = + if s.Contains '`' then + Error + <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot contain backtick" ] + else + Ok <| s + + let notContainsTilde fieldName (s: string) = + if s.Contains '~' then + Error + <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot contain tilde" ] + else + Ok <| s + + let notContainsDash fieldName (s: string) = + if s.Contains '-' then + Error + <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot contain dash: '-'" ] + else + Ok <| s + + let notContainsUmlauts fieldName (s: string) = + let umlauts = [ 'ä'; 'ö'; 'ü'; 'ß'; 'Ä'; 'Ö'; 'Ü' ] |> set + + let contains = s |> Seq.exists (fun c -> umlauts |> Set.contains c) + + if contains then + Error + <| ValidationErrors.create fieldName [ + $"'%s{fieldName}' field cannot contain umlauts: ä, ö, ü, ß, Ä, Ö, Ü" + ] + else + Ok <| s + + open Validus.Operators + + let allowEmpty = ValueOption.ofObj >> ValueOption.filter (not << String.IsNullOrWhiteSpace) + + let validateStringCharacters = + notStartsWithWhiteSpace + <+> notEndsWithWhiteSpace + <+> notContainsTilde + <+> notContainsUmlauts + <+> notContainsBacktick + + module Uri = + + let isValid fieldName uriString = + if Uri.IsWellFormedUriString(uriString, UriKind.Absolute) then + Ok uriString + else + Error + <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field is not a valid URI" ] + + +//module VOptionString = + +// let allow (validator : Validator) : Validator = +// fun fieldName (value : string voption) -> +// match value with +// | ValueNone -> Ok ValueNone +// | ValueSome str -> (validator *|* ValueSome) fieldName str + +// let toValidationResult _ value : ValidationResult = +// let valueOption = +// value +// |> ValueOption.ofObj +// |> ValueOption.filter (not << String.IsNullOrWhiteSpace) +// match valueOption with +// | ValueSome str -> ValueSome str |> Ok +// | ValueNone -> ValueNone |> Ok + +module ValidationErrors = + + let toIGQLErrors (errors: ValidationErrors) : IGQLError list = + errors + |> ValidationErrors.toList + |> List.map (fun e -> { new IGQLError with member _.Message = e }) + +module Operators = + + let vOption (v1: 'a -> 'a voption) (v2: Validator<'a, 'b>) : Validator<'a, 'b voption> = + fun x y -> + let value = v1 y + match value with + | ValueSome value -> (v2 *|* ValueSome) x y + | ValueNone -> Ok ValueNone + + let (?=>) v1 v2 = vOption v1 v2 + let (?=<) v2 v1 = vOption v1 v2 + +module Scalars = + + open System.Text.Json + open FSharp.Data.GraphQL.Ast + open FSharp.Data.GraphQL.Types + open FSharp.Data.GraphQL.Types.SchemaDefinitions.Errors + + type Define with + + static member ValidStringScalar<'t>(typeName, createValid : Validator, ?description: string) = + let createValid = createValid typeName + Define.WrappedScalar + (name = typeName, + coerceInput = + (function + | Variable e when e.ValueKind = JsonValueKind.String -> e.GetString() |> createValid |> Result.mapError ValidationErrors.toIGQLErrors + | InlineConstant (StringValue s) -> s |> createValid |> Result.mapError ValidationErrors.toIGQLErrors + | Variable e -> e.GetDeserializeError typeName + | InlineConstant value -> value.GetCoerceError typeName), + coerceOutput = + (function + | :? ('t voption) as x -> x |> string |> Some + | :? 't as x -> Some (string x) + | :? string as s -> s |> Some + | null -> None + | _ -> raise <| System.NotSupportedException ()), + ?description = description) diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs new file mode 100644 index 000000000..34717d2ad --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs @@ -0,0 +1,244 @@ +// The MIT License (MIT) + +[] +module FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests + +#nowarn "25" + +open Xunit +open System +open System.Collections.Immutable +open System.Text.Json + +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Types +open FSharp.Data.GraphQL.Parser +open FSharp.Data.GraphQL.Samples.StarWarsApi + +module Phantom = + + type ZipCode = interface end + type City = interface end + type State = interface end + + module Address = + + type Line1 = interface end + type Line2 = interface end + +open FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests + +type AddressLine1 = ValidString +type AddressLine2 = ValidString +type City = ValidString +type State = ValidString +type ZipCode = ValidString + +type AddressRecord = { + Line1: AddressLine1 voption + Line2: AddressLine2 voption + City: City voption + State: State voption + ZipCode: ZipCode voption +} + +type AddressClass(zipCode, city, state, line1, line2) = + member _.Line1 : AddressLine1 voption = line1 + member _.Line2 : AddressLine2 voption = line2 + member _.City : City voption = city + member _.State : State voption = state + member _.ZipCode : ZipCode voption = zipCode + +[] +type AddressStruct ( + zipCode : ZipCode voption, + city : City voption, + state : State voption, + line1 : AddressLine1 voption, + line2 : AddressLine2 voption +) = + member _.Line1 = line1 + member _.Line2 = line2 + member _.City = city + member _.State = state + member _.ZipCode = zipCode + + +open Validus +open FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests.String +open FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests.Operators + +[] +module State = + + open ValidString + open Validus.Operators + + let create : Validator = + (Check.String.lessThanLen 100 <+> validateStringCharacters) *|* ValidString + + let createOrWhitespace : Validator = + (allowEmpty ?=> (Check.String.lessThanLen 100 <+> validateStringCharacters)) *|* ValueOption.map ValidString + +module Address = + + open ValidString + open Validus.Operators + + let createLine1 : Validator = + (allowEmpty ?=> (Check.String.lessThanLen 1000 <+> validateStringCharacters)) *|* ValueOption.map ValidString + + let createLine2 : Validator = + (allowEmpty ?=> (Check.String.lessThanLen 1000 <+> validateStringCharacters)) *|* ValueOption.map ValidString + + let createZipCode : Validator = + (allowEmpty ?=> (Check.String.lessThanLen 100 <+> validateStringCharacters)) *|* ValueOption.map ValidString + + let createCity : Validator = + (allowEmpty ?=> (Check.String.lessThanLen 100 <+> validateStringCharacters)) *|* ValueOption.map ValidString + + + open Scalars + + let Line1Type = Define.ValidStringScalar("AddressLine1", createLine1, "Address line 1") + let Line2Type = Define.ValidStringScalar("AddressLine2", createLine2, "Address line 2") + let ZipCodeType = Define.ValidStringScalar("AddressZipCode", createZipCode, "Address zip code") + let CityType = Define.ValidStringScalar("City", createCity) + let StateType = Define.ValidStringScalar("State", State.createOrWhitespace) + +let InputAddressRecordType = + Define.InputObject( + name = "InputAddressRecord", + fields = [ + Define.Input("line1", Nullable Address.Line1Type) + Define.Input("line2", Nullable Address.Line2Type) + Define.Input("zipCode", Nullable Address.ZipCodeType) + Define.Input("city", Nullable Address.CityType) + Define.Input("state", Nullable Address.StateType) + ] + ) + +let InputAddressClassType = + Define.InputObject( + name = "InputAddressObject", + fields = [ + Define.Input("line1", Nullable Address.Line1Type) + Define.Input("line2", Nullable Address.Line2Type) + Define.Input("zipCode", Nullable Address.ZipCodeType) + Define.Input("city", Nullable Address.CityType) + Define.Input("state", Nullable Address.StateType) + ] + ) + +let InputAddressStructType = + Define.InputObject( + name = "InputAddressStruct", + fields = [ + Define.Input("line1", Nullable Address.Line1Type) + Define.Input("line2", Nullable Address.Line2Type) + Define.Input("zipCode", Nullable Address.ZipCodeType) + Define.Input("city", Nullable Address.CityType) + Define.Input("state", Nullable Address.StateType) + ] + ) + +open FSharp.Data.GraphQL.Execution +open FSharp.Data.GraphQL.Validation +open FSharp.Data.GraphQL.Validation.ValidationResult +open ErrorHelpers + +let createSingleError message = + [{ new IGQLError with member _.Message = message }] + +type InputRecordNested = { HomeAddress : AddressRecord; WorkAddress : AddressRecord option; MailingAddress : AddressRecord voption } + +let InputRecordNestedType = + Define.InputObject ( + "InputRecordNested", + [ Define.Input ("homeAddress", InputAddressRecordType) + Define.Input ("workAddress", Nullable InputAddressRecordType) + Define.Input ("mailingAddress", Nullable InputAddressRecordType) ], + fun inputRecord -> + match inputRecord.MailingAddress, inputRecord.WorkAddress with + | ValueNone, None -> ValidationError <| createSingleError "MailingAddress or WorkAddress must be provided" + | _ -> Success + @@ + if inputRecord.MailingAddress.IsSome && inputRecord.HomeAddress = inputRecord.MailingAddress.Value then + ValidationError <| createSingleError "HomeAddress and MailingAddress must be different" + else + Success + ) + +let schema = + let schema = + Schema ( + query = + Define.Object ( + "Query", + fun () -> + [ Define.Field ( + "recordInputs", + StringType, + [ Define.Input ("record", InputAddressRecordType) + Define.Input ("recordOptional", Nullable InputAddressRecordType) + Define.Input ("recordNested", Nullable InputRecordNestedType) ], + stringifyInput + ) // TODO: add all args stringificaiton + Define.Field ( + "objectInputs", + StringType, + [ Define.Input ("object", InputAddressClassType) + Define.Input ("objectOptional", Nullable InputAddressClassType) ], + stringifyInput + ) // TODO: add all args stringificaiton + Define.Field ( + "structInputs", + StringType, + [ Define.Input ("struct", InputAddressStructType) + Define.Input ("structOptional", Nullable InputAddressStructType) ], + stringifyInput + ) ] // TODO: add all args stringificaiton + ) + ) + + Executor schema + + +[] +let ``Execute handles validation of valid inline input records with all fields`` () = + let query = + """{ + recordInputs( + record: { zipCode: "12345", city: "Miami" }, + recordOptional: { zipCode: "12345", city: "Miami" }, + recordNested: { homeAddress: { zipCode: "12345", city: "Miami" }, workAddress: { zipCode: "67890", city: "Miami" } } + ) + objectInputs( + object: { zipCode: "12345", city: "Miami" }, + objectOptional: { zipCode: "12345", city: "Miami" } + ) + structInputs( + struct: { zipCode: "12345", city: "Miami" }, + structOptional: { zipCode: "12345", city: "Miami" } + ) + }""" + let result = sync <| schema.AsyncExecute(parse query) + ensureDirect result <| fun data errors -> empty errors + +[] +let ``Execute handles validation of valid inline input records with mandatory-only fields`` () = + let query = + """{ + recordInputs( + record: { zipCode: "12345", city: "Miami" }, + recordNested: { homeAddress: { zipCode: "12345", city: "Miami" }, workAddress: { zipCode: "67890", city: "Miami" } } + ) + objectInputs( + object: { zipCode: "12345", city: "Miami" }, + ) + structInputs( + struct: { zipCode: "12345", city: "Miami" }, + ) + }""" + let result = sync <| schema.AsyncExecute(parse query) + ensureDirect result <| fun data errors -> empty errors From 96dbd314b62cf6dfcebd08770e3759ae8a103632 Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 14 Feb 2024 11:05:32 +0100 Subject: [PATCH 049/100] Adding HttpContext as parameter to root factory --- .../Giraffe/HttpHandlers.fs | 2 +- src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs | 3 ++- .../GraphQLWebsocketMiddleware.fs | 6 +++--- .../StartupExtensions.fs | 5 +++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index dc167b508..5c1c5a106 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -136,7 +136,7 @@ module HttpHandlers = logger.LogDebug(sprintf "Received query: %A" query) else () - let root = rootFactory() + let root = rootFactory(ctx) let! result = executor.AsyncExecute( query.ExecutionPlan, diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs index a85484fd5..5468d8a2b 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs @@ -4,6 +4,7 @@ open FSharp.Data.GraphQL open System open System.Text.Json open System.Threading.Tasks +open Microsoft.AspNetCore.Http type PingHandler = IServiceProvider -> JsonDocument option -> Task @@ -15,7 +16,7 @@ type GraphQLTransportWSOptions = type GraphQLOptions<'Root> = { SchemaExecutor: Executor<'Root> - RootFactory: unit -> 'Root + RootFactory: HttpContext -> 'Root SerializerOptions: JsonSerializerOptions WebsocketOptions: GraphQLTransportWSOptions } \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 418cf4679..85732d710 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -167,7 +167,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti let tryToGracefullyCloseSocketWithDefaultBehavior = tryToGracefullyCloseSocket (WebSocketCloseStatus.NormalClosure, "Normal Closure") - let handleMessages (cancellationToken: CancellationToken) (serializerOptions: JsonSerializerOptions) (executor : Executor<'Root>) (root: unit -> 'Root) (pingHandler : PingHandler option) (socket : WebSocket) = + let handleMessages (cancellationToken: CancellationToken) (httpContext: HttpContext) (serializerOptions: JsonSerializerOptions) (executor : Executor<'Root>) (root: HttpContext -> 'Root) (pingHandler : PingHandler option) (socket : WebSocket) = let subscriptions = new Dictionary() // ----------> // Helpers --> @@ -274,7 +274,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti CancellationToken.None) else let! planExecutionResult = - executor.AsyncExecute(query.ExecutionPlan, root(), query.Variables) + executor.AsyncExecute(query.ExecutionPlan, root(httpContext), query.Variables) |> Async.StartAsTask do! planExecutionResult |> applyPlanExecutionResult id socket @@ -365,7 +365,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti let safe_HandleMessages = handleMessages longRunningCancellationToken try do! socket - |> safe_HandleMessages options.SerializerOptions options.SchemaExecutor options.RootFactory options.WebsocketOptions.CustomPingHandler + |> safe_HandleMessages ctx options.SerializerOptions options.SchemaExecutor options.RootFactory options.WebsocketOptions.CustomPingHandler with | ex -> logger.LogError(ex, "Cannot handle Websocket message.") diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index ccfa00aa8..9ad3eb23c 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -5,6 +5,7 @@ open Microsoft.AspNetCore.Builder open Microsoft.Extensions.DependencyInjection open System.Runtime.CompilerServices open System.Text.Json +open Microsoft.AspNetCore.Http [] type ServiceCollectionExtensions() = @@ -22,14 +23,14 @@ type ServiceCollectionExtensions() = } [] - static member AddGraphQLOptions<'Root>(this : IServiceCollection, executor : Executor<'Root>, rootFactory : unit -> 'Root, endpointUrl : string) = + static member AddGraphQLOptions<'Root>(this : IServiceCollection, executor : Executor<'Root>, rootFactory : HttpContext -> 'Root, endpointUrl : string) = this.AddSingleton>(createStandardOptions executor rootFactory endpointUrl) [] static member AddGraphQLOptionsWith<'Root> ( this : IServiceCollection, executor : Executor<'Root>, - rootFactory : unit -> 'Root, + rootFactory : HttpContext -> 'Root, endpointUrl : string, extraConfiguration : GraphQLOptions<'Root> -> GraphQLOptions<'Root> ) = From 9fe5f2e20e79c357994391d4e52ce3d68ec92ef4 Mon Sep 17 00:00:00 2001 From: valber Date: Mon, 19 Feb 2024 22:50:08 +0100 Subject: [PATCH 050/100] Adjusting ...Server.AspNetCore's branch code to latest `dev` stage This is still a work in progress. Some changes are still needed to consolidate the approach. --- ...rp.Data.GraphQL.Samples.StarWarsApi.fsproj | 6 - samples/star-wars-api/Startup.fs | 23 ++- .../star-wars-api/WebSocketJsonConverters.fs | 182 ------------------ ...harp.Data.GraphQL.Server.AspNetCore.fsproj | 2 +- .../Giraffe/HttpHandlers.fs | 82 +++++--- .../GraphQLWebsocketMiddleware.fs | 48 +++-- .../Serialization/GraphQLQueryDecoding.fs | 8 + .../Serialization/JsonConverters.fs | 17 ++ .../StartupExtensions.fs | 2 +- ...ata.GraphQL.IntegrationTests.Server.fsproj | 6 +- .../Startup.fs | 33 +++- .../introspection.json | 7 +- .../AspNetCore/TestSchema.fs | 30 +-- 13 files changed, 180 insertions(+), 266 deletions(-) delete mode 100644 samples/star-wars-api/WebSocketJsonConverters.fs diff --git a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj index ef3512fa2..1df66aeba 100644 --- a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj +++ b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj @@ -17,18 +17,12 @@ - - - - - - diff --git a/samples/star-wars-api/Startup.fs b/samples/star-wars-api/Startup.fs index b77e53638..a8e66c326 100644 --- a/samples/star-wars-api/Startup.fs +++ b/samples/star-wars-api/Startup.fs @@ -13,11 +13,19 @@ open Microsoft.Extensions.Logging open System open Microsoft.AspNetCore.Server.Kestrel.Core open Microsoft.Extensions.Hosting +open Microsoft.Extensions.Options + +// See https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions +type MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions +// See https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.json.jsonoptions +type HttpClientJsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions + +module Constants = + let [] Indented = "Indented" type Startup private () = - let rootFactory (ctx) : Root = - { RequestId = Guid.NewGuid().ToString() } + let rootFactory (ctx) : Root = Root(ctx) new (configuration: IConfiguration) as this = Startup() then @@ -37,7 +45,7 @@ type Startup private () = ) // Use for pretty printing in logs .Configure( - Constants.Idented, + Constants.Indented, Action(fun o -> Json.configureDefaultSerializerOptions Seq.empty o.SerializerOptions o.SerializerOptions.WriteIndented <- true @@ -69,9 +77,10 @@ type Startup private () = .UseGiraffeErrorHandler(errorHandler) .UseWebSockets() .UseWebSocketsForGraphQL() - .UseGiraffe (HttpHandlers.handleGraphQL - applicationLifetime.ApplicationStopping - (loggerFactory.CreateLogger("HttpHandlers.handlerGraphQL")) - ) + .UseGiraffe + (HttpHandlers.handleGraphQLWithResponseInterception + applicationLifetime.ApplicationStopping + (loggerFactory.CreateLogger("HttpHandlers.handlerGraphQL")) + (setHttpHeader "Request-Type" "Classic")) member val Configuration : IConfiguration = null with get, set diff --git a/samples/star-wars-api/WebSocketJsonConverters.fs b/samples/star-wars-api/WebSocketJsonConverters.fs deleted file mode 100644 index e0cee347d..000000000 --- a/samples/star-wars-api/WebSocketJsonConverters.fs +++ /dev/null @@ -1,182 +0,0 @@ -namespace FSharp.Data.GraphQL.Samples.StarWarsApi - -open System -open System.Collections.Generic -open System.Collections.Immutable -open System.Text.Json -open System.Text.Json.Nodes -open System.Text.Json.Serialization - -open FSharp.Data.GraphQL -open FSharp.Data.GraphQL.Types - -type private GQLEditableRequestContent = - { Query : string - OperationName : string Skippable - Variables : JsonObject Skippable } - -[] -module JsonNodeExtensions = - - open System.Buffers - - type JsonNode with - - static member Create (object: 'T) = - let bufferWriter = new ArrayBufferWriter(); - use writer = new Utf8JsonWriter(bufferWriter) - JsonSerializer.Serialize<'T>(writer, object, Json.serializerOptions) - JsonSerializer.Deserialize(bufferWriter.WrittenSpan, Json.serializerOptions) - - member node.AsJsonElement() = - let bufferWriter = new ArrayBufferWriter() - use writer = new Utf8JsonWriter(bufferWriter) - node.WriteTo (writer, Json.serializerOptions) - let bytes = bufferWriter.WrittenSpan - let mutable reader = new Utf8JsonReader(bytes) - JsonDocument.ParseValue(&reader).RootElement - -[] -type GraphQLQueryConverter<'a>(executor : Executor<'a>, replacements: Map, ?meta : Metadata) = - inherit JsonConverter() - - /// Active pattern to match GraphQL type defintion with nullable / optional types. - let (|Nullable|_|) (tdef : TypeDef) = - match tdef with - | :? NullableDef as x -> Some x.OfType - | _ -> None - - override __.CanConvert(t) = t = typeof - - override __.Write(_, _, _) = raise <| System.NotSupportedException() - - override __.Read(reader, _, options) = - - let request = JsonSerializer.Deserialize(&reader, options) - let result = - let query = request.Query - match meta with - | Some meta -> executor.CreateExecutionPlan(query, meta = meta) - | None -> executor.CreateExecutionPlan(query) - match result with - | Result.Error struct (_, errors) -> - failwith (String.concat Environment.NewLine (errors |> Seq.map (fun error -> error.Message))) - | Ok executionPlan when executionPlan.Variables = [] -> { ExecutionPlan = executionPlan; Variables = ImmutableDictionary.Empty } - | Ok executionPlan -> - match request.Variables with - | Skip -> failwith "No variables provided" - | Include vars -> - // For multipart requests, we need to replace some variables - // TODO: Implement JSON path - Map.iter (fun path rep -> - vars.Remove path |> ignore - vars.Add(path, JsonNode.Create rep)) - replacements - //Map.iter(fun path rep -> vars.SelectToken(path).Replace(JObject.FromObject(rep))) replacements - let variables = - executionPlan.Variables - |> List.fold (fun (acc: ImmutableDictionary.Builder) (vdef: VarDef) -> - match vars.TryGetPropertyValue vdef.Name with - | true, jsonNode -> - let jsonElement = jsonNode.AsJsonElement() - acc.Add (vdef.Name, jsonElement) - | false, _ -> - match vdef.DefaultValue, vdef.TypeDef with - | Some _, _ -> () - | _, Nullable _ -> () - | None, _ -> failwithf "A variable '$%s' has no default value and is missing!" vdef.Name - acc) - (ImmutableDictionary.CreateBuilder()) - { ExecutionPlan = executionPlan; Variables = variables.ToImmutable() } - -[] -module private GraphQLSubscriptionFields = - let [] FIELD_Type = "type" - let [] FIELD_Id = "id" - let [] FIELD_Payload = "payload" - let [] FIELD_Error = "error" - -[] -type WebSocketClientMessageConverter<'a>(executor : Executor<'a>, replacements: Map, ?meta : Metadata) = - inherit JsonConverter() - - override __.CanConvert(t) = t = typeof - - override __.Write(_, _, _) = raise <| NotSupportedException() - - override __.Read(reader, _, options) = - let properties = JsonSerializer.Deserialize>(&reader, options) - let typ = properties.["type"] - if typ.ValueKind = JsonValueKind.String then - let value = typ.GetString() - match value with - | "connection_init" -> ConnectionInit - | "connection_terminate" -> ConnectionTerminate - | "start" -> - let id = - match properties.TryGetValue FIELD_Id with - | true, value -> ValueSome <| value.GetString () - | false, _ -> ValueNone - let payload = - match properties.TryGetValue FIELD_Payload with - | true, value -> ValueSome <| value - | false, _ -> ValueNone - match id, payload with - | ValueSome id, ValueSome payload -> - try - let queryConverter = - match meta with - | Some meta -> GraphQLQueryConverter(executor, replacements, meta) :> JsonConverter - | None -> GraphQLQueryConverter(executor, replacements) :> JsonConverter - let options' = Json.getSerializerOptions (Seq.singleton queryConverter) - let req = payload.Deserialize options' - Start(id, req) - with e -> ParseError(Some id, "Parse Failed with Exception: " + e.Message) - | ValueNone, _ -> ParseError(None, "Malformed GQL_START message, expected id field but found none") - | _, ValueNone -> ParseError(None, "Malformed GQL_START message, expected payload field but found none") - | "stop" -> - match properties.TryGetValue FIELD_Id with - | true, id -> Stop(id.GetString ()) - | false, _ -> ParseError(None, "Malformed GQL_STOP message, expected id field but found none") - | _ -> - ParseError(None, $"Message Type '%s{typ.GetRawText()}' is not supported!") - else - ParseError(None, $"Message Type must be string but got {Environment.NewLine}%s{typ.GetRawText()}") - -[] -type WebSocketServerMessageConverter() = - inherit JsonConverter() - - override __.CanConvert(t) = t = typedefof || t.DeclaringType = typedefof - - override __.Read(_, _, _) = raise <| NotSupportedException() - - override __.Write(writer, value, options) = - writer.WriteStartObject() - match value with - | ConnectionAck -> - writer.WriteString(FIELD_Type, "connection_ack") - | ConnectionError(err) -> - writer.WriteString(FIELD_Type, "connection_error") - writer.WritePropertyName(FIELD_Payload) - writer.WriteStartObject() - writer.WriteString(FIELD_Error, err) - writer.WriteEndObject() - | Error(id, err) -> - writer.WriteString(FIELD_Type, "error") - writer.WritePropertyName(FIELD_Payload) - writer.WriteStartObject() - writer.WriteString(FIELD_Error, err) - writer.WriteEndObject() - match id with - | Some id -> writer.WriteString (FIELD_Id, id) - | None -> writer.WriteNull(FIELD_Id) - | Data(id, result) -> - writer.WriteString(FIELD_Type, "data") - writer.WriteString(FIELD_Id, id) - writer.WritePropertyName(FIELD_Payload) - JsonSerializer.Serialize(writer, result, options) - | Complete(id) -> - writer.WriteString(FIELD_Type, "complete") - writer.WriteString(FIELD_Id, id) - writer.WriteEndObject() diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj index e493332c4..6d5c60b8f 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj @@ -1,7 +1,7 @@  - net6.0 + $(PackageTargetFrameworks) true true FSharp implementation of Facebook GraphQL query language (Application Infrastructure) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 5c1c5a106..79f89ac3f 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -16,9 +16,11 @@ open System.Threading.Tasks type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult module HttpHandlers = + open System.Collections.Immutable - let private httpOk (cancellationToken : CancellationToken) (serializerOptions : JsonSerializerOptions) payload : HttpHandler = + let private httpOk (cancellationToken : CancellationToken) (customHandler : HttpHandler) (serializerOptions : JsonSerializerOptions) payload : HttpHandler = setStatusCode 200 + >=> customHandler >=> (setHttpHeader "Content-Type" "application/json") >=> (fun _ ctx -> JsonSerializer @@ -44,12 +46,14 @@ module HttpHandlers = ] ) - let private addToErrorsInData (theseNew: Error list) (data : IDictionary) = - let toNameValueLookupList (errors : Error list) = + let private addToErrorsInData (theseNew: GQLProblemDetails list) (data : IDictionary) = + let toNameValueLookupList (errors : GQLProblemDetails list) = errors |> List.map - (fun (errMsg, path) -> - NameValueLookup.ofList ["message", upcast errMsg; "path", upcast path] + (fun (error) -> + NameValueLookup.ofList + ["message", upcast error.Message + "path", upcast error.Path] ) let result = data @@ -73,11 +77,12 @@ module HttpHandlers = "errors" (upcast (theseNew |> toNameValueLookupList)) - let handleGraphQL<'Root> - (cancellationToken : CancellationToken) - (logger : ILogger) - (next : HttpFunc) (ctx : HttpContext) = - task { + let handleGraphQLWithResponseInterception<'Root> + (cancellationToken : CancellationToken) + (logger : ILogger) + (interceptor : HttpHandler) + (next : HttpFunc) (ctx : HttpContext) = + task { let cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ctx.RequestAborted).Token if cancellationToken.IsCancellationRequested then return (fun _ -> None) ctx @@ -97,23 +102,27 @@ module HttpHandlers = | :? GraphQLException as ex -> Task.FromResult(fail (sprintf "%s" (ex.Message))) - let applyPlanExecutionResult (result : GQLResponse) = + let applyPlanExecutionResult (result : GQLExecutionResult) = task { - match result with - | Direct (data : IDictionary, errs) -> - let finalData = data |> addToErrorsInData errs - return! httpOk cancellationToken serializerOptions finalData next ctx - | other -> - let error = - prepareGenericErrors ["subscriptions are not supported here (use the websocket endpoint instead)."] - return! httpOk cancellationToken serializerOptions error next ctx + let gqlResponse = + match result.Content with + | Direct (data, errs) -> + GQLResponse.Direct(result.DocumentId, data, errs) + | _ -> + GQLResponse.RequestError( + result.DocumentId, + [ GQLProblemDetails.Create( + "subscriptions are not supported here (use the websocket endpoint instead)." + )] + ) + return! httpOk cancellationToken interceptor serializerOptions gqlResponse next ctx } let handleDeserializedGraphQLRequest (graphqlRequest : GraphQLRequest) = task { match graphqlRequest.Query with | None -> - let! result = executor.AsyncExecute (Introspection.IntrospectionQuery) |> Async.StartAsTask + let! result = executor.AsyncExecute (IntrospectionQuery.Definition) |> Async.StartAsTask if logger.IsEnabled(LogLevel.Debug) then logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) else @@ -130,7 +139,7 @@ module HttpHandlers = match graphQLQueryDecodingResult with | Failure errMsgs -> return! - httpOk cancellationToken serializerOptions (prepareGenericErrors errMsgs) next ctx + httpOk cancellationToken interceptor serializerOptions (prepareGenericErrors errMsgs) next ctx | Success (query, _) -> if logger.IsEnabled(LogLevel.Debug) then logger.LogDebug(sprintf "Received query: %A" query) @@ -138,11 +147,15 @@ module HttpHandlers = () let root = rootFactory(ctx) let! result = - executor.AsyncExecute( - query.ExecutionPlan, - data = root, - variables = query.Variables - )|> Async.StartAsTask + let variables = ImmutableDictionary.CreateRange( + query.Variables + |> Map.map (fun _ value -> value :?> JsonElement) + ) + executor.AsyncExecute( + query.ExecutionPlan, + data = root, + variables = variables + )|> Async.StartAsTask if logger.IsEnabled(LogLevel.Debug) then logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) else @@ -150,7 +163,7 @@ module HttpHandlers = return! result |> applyPlanExecutionResult } if ctx.Request.Headers.ContentLength.GetValueOrDefault(0) = 0 then - let! result = executor.AsyncExecute (Introspection.IntrospectionQuery) |> Async.StartAsTask + let! result = executor.AsyncExecute (IntrospectionQuery.Definition) |> Async.StartAsTask if logger.IsEnabled(LogLevel.Debug) then logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) else @@ -159,7 +172,18 @@ module HttpHandlers = else match! deserializeGraphQLRequest() with | Failure errMsgs -> - return! httpOk cancellationToken serializerOptions (prepareGenericErrors errMsgs) next ctx + return! httpOk cancellationToken interceptor serializerOptions (prepareGenericErrors errMsgs) next ctx | Success (graphqlRequest, _) -> return! handleDeserializedGraphQLRequest graphqlRequest - } \ No newline at end of file + } + + let handleGraphQL<'Root> + (cancellationToken : CancellationToken) + (logger : ILogger) + (next : HttpFunc) (ctx : HttpContext) = + handleGraphQLWithResponseInterception<'Root> + cancellationToken + logger + id + next + ctx diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 85732d710..27de87302 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -12,6 +12,7 @@ open System.Threading.Tasks open FSharp.Data.GraphQL.Execution open Microsoft.Extensions.Hosting open Microsoft.Extensions.Logging +open System.Collections.Immutable type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifetime : IHostApplicationLifetime, serviceProvider : IServiceProvider, logger : ILogger>, options : GraphQLOptions<'Root>) = @@ -125,12 +126,12 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti let addClientSubscription (id : SubscriptionId) - (howToSendDataOnNext: SubscriptionId -> Output -> Task) + (howToSendDataOnNext: SubscriptionId -> 'ResponseContent -> Task) ( subscriptions : SubscriptionsDict, socket : WebSocket, - streamSource: IObservable, + streamSource: IObservable<'ResponseContent>, jsonSerializerOptions : JsonSerializerOptions ) = - let observer = new Reactive.AnonymousObserver( + let observer = new Reactive.AnonymousObserver<'ResponseContent>( onNext = (fun theOutput -> theOutput @@ -179,9 +180,8 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti socket |> rcvMsgViaSocket serializerOptions - let sendOutput id output = - let outputAsDict = output :> IDictionary - match outputAsDict.TryGetValue("errors") with + let sendOutput id (output : Output) = + match output.TryGetValue("errors") with | true, theValue -> // The specification says: "This message terminates the operation and no further messages will be sent." subscriptions @@ -190,20 +190,39 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti | false, _ -> sendMsg (Next (id, output)) - let sendOutputDelayedBy (cancToken: CancellationToken) (ms: int) id output = + let sendSubscriptionResponseOutput id subscriptionResult = + match subscriptionResult with + | SubscriptionResult output -> + output + |> sendOutput id + | SubscriptionErrors (output, errors) -> + printfn "Subscription errors: %s" (String.Join('\n', errors |> Seq.map (fun x -> sprintf "- %s" x.Message))) + Task.FromResult(()) + + let sendDeferredResponseOutput id deferredResult = + match deferredResult with + | DeferredResult (obj, path) -> + let output = obj :?> Dictionary + output + |> sendOutput id + | DeferredErrors (obj, errors, _) -> + printfn "Deferred response errors: %s" (String.Join('\n', errors |> Seq.map (fun x -> sprintf "- %s" x.Message))) + Task.FromResult(()) + + let sendDeferredResultDelayedBy (cancToken: CancellationToken) (ms: int) id deferredResult = task { do! Async.StartAsTask(Async.Sleep ms, cancellationToken = cancToken) - do! output - |> sendOutput id + do! deferredResult + |> sendDeferredResponseOutput id } - let sendQueryOutputDelayedBy = sendOutputDelayedBy cancellationToken + let sendQueryOutputDelayedBy = sendDeferredResultDelayedBy cancellationToken - let applyPlanExecutionResult (id: SubscriptionId) (socket) (executionResult: GQLResponse) = + let applyPlanExecutionResult (id: SubscriptionId) (socket) (executionResult: GQLExecutionResult) = task { match executionResult with | Stream observableOutput -> (subscriptions, socket, observableOutput, serializerOptions) - |> addClientSubscription id sendOutput + |> addClientSubscription id sendSubscriptionResponseOutput | Deferred (data, errors, observableOutput) -> do! data |> sendOutput id @@ -215,6 +234,8 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti | Direct (data, _) -> do! data |> sendOutput id + | RequestError problemDetails -> + printfn "Request error: %s" (String.Join('\n', problemDetails |> Seq.map (fun x -> sprintf "- %s" x.Message))) } let getStrAddendumOfOptionalPayload optionalPayload = @@ -273,8 +294,9 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti sprintf "Subscriber for %s already exists" id, CancellationToken.None) else + let variables = ImmutableDictionary.CreateRange(query.Variables |> Map.map (fun _ value -> value :?> JsonElement)) let! planExecutionResult = - executor.AsyncExecute(query.ExecutionPlan, root(httpContext), query.Variables) + executor.AsyncExecute(query.ExecutionPlan, root(httpContext), variables) |> Async.StartAsTask do! planExecutionResult |> applyPlanExecutionResult id socket diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs index 0b4e386d6..281ad3a2c 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs @@ -62,6 +62,14 @@ module GraphQLQueryDecoding = fail (sprintf "%s" (ex.Message)) executionPlanResult + |> bindR + (function + | Ok x -> Success (x, []) + | Result.Error (_, problemDetails) -> + Failure <| + (problemDetails + |> List.map (fun (x: GQLProblemDetails) -> x.Message)) + ) |> bindR (fun executionPlan -> match variables with diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index f866ff898..705252c92 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -154,8 +154,25 @@ type RawServerMessageConverter() = module JsonConverterUtils = + + let [] UnionTag = "kind" + + let private defaultJsonFSharpOptions = + JsonFSharpOptions( + JsonUnionEncoding.InternalTag + ||| JsonUnionEncoding.AllowUnorderedTag + ||| JsonUnionEncoding.NamedFields + ||| JsonUnionEncoding.UnwrapSingleCaseUnions + ||| JsonUnionEncoding.UnwrapRecordCases + ||| JsonUnionEncoding.UnwrapOption + ||| JsonUnionEncoding.UnwrapFieldlessTags, + UnionTag, + allowOverride = true) let configureSerializer (executor : Executor<'Root>) (jsonSerializerOptions : JsonSerializerOptions) = jsonSerializerOptions.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase + jsonSerializerOptions.PropertyNameCaseInsensitive <- true + jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()) jsonSerializerOptions.Converters.Add(new ClientMessageConverter<'Root>(executor)) jsonSerializerOptions.Converters.Add(new RawServerMessageConverter()) + jsonSerializerOptions |> defaultJsonFSharpOptions.AddToJsonSerializerOptions jsonSerializerOptions \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index 9ad3eb23c..c4182346d 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -14,7 +14,7 @@ type ServiceCollectionExtensions() = { SchemaExecutor = executor RootFactory = rootFactory SerializerOptions = - JsonSerializerOptions() + JsonSerializerOptions(IgnoreNullValues = true) |> JsonConverterUtils.configureSerializer executor WebsocketOptions = { EndpointUrl = endpointUrl diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj index 679dfc155..79c803a84 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj @@ -17,10 +17,6 @@ - - - - @@ -28,7 +24,6 @@ - @@ -36,5 +31,6 @@ + diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs index 2680d31e8..754040a9e 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs @@ -2,6 +2,7 @@ namespace FSharp.Data.GraphQL.IntegrationTests.Server open System open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Server.Kestrel.Core open Microsoft.Extensions.Configuration open Microsoft.Extensions.DependencyInjection @@ -9,9 +10,24 @@ open Microsoft.Extensions.Logging open Microsoft.Extensions.Options open Giraffe +open FSharp.Data.GraphQL.Server.AspNetCore +open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe open FSharp.Data.GraphQL.Samples.StarWarsApi +open Microsoft.Extensions.Hosting + +// See https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions +type MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions +// See https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.json.jsonoptions +type HttpClientJsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions + +module Constants = + let [] Indented = "Indented" type Startup private () = + + let rootFactory (httpContext: HttpContext) : Root = + Root(httpContext) + new (configuration: IConfiguration) as this = Startup() then this.Configuration <- configuration @@ -21,6 +37,11 @@ type Startup private () = .AddGiraffe() .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) + .AddGraphQLOptions( + Schema.executor, + rootFactory, + "/ws" + ) // Surprisingly minimal APIs use Microsoft.AspNetCore.Http.Json.JsonOptions // Use if you want to return HTTP responses using minmal APIs IResult interface .Configure( @@ -28,9 +49,9 @@ type Startup private () = Json.configureDefaultSerializerOptions Seq.empty o.SerializerOptions ) ) - // Use for pretty printing in logs + // // Use for pretty printing in logs .Configure( - Constants.Idented, + Constants.Indented, Action(fun o -> Json.configureDefaultSerializerOptions Seq.empty o.SerializerOptions o.SerializerOptions.WriteIndented <- true @@ -47,8 +68,14 @@ type Startup private () = let errorHandler (ex : Exception) (log : ILogger) = log.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.") clearResponse >=> setStatusCode 500 + let applicationLifeTime = app.ApplicationServices.GetRequiredService() + let loggerFactory = app.ApplicationServices.GetRequiredService() app .UseGiraffeErrorHandler(errorHandler) - .UseGiraffe HttpHandlers.webApp + .UseGiraffe + (HttpHandlers.handleGraphQLWithResponseInterception + applicationLifeTime.ApplicationStopping + (loggerFactory.CreateLogger("HttpHandlers.handleGraphQL")) + (setHttpHeader "Request-Type" "Classic")) member val Configuration : IConfiguration = null with get, set diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json index 8128e5bdc..c3fe6fa10 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json @@ -1,5 +1,5 @@ { - "documentId": 1360354553, + "documentId": -727244275, "data": { "__schema": { "queryType": { @@ -1749,6 +1749,5 @@ } ] } - }, - "documentId": 251746993 -} + } +} \ No newline at end of file diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs index f2f2bbc42..e44f0ca73 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs @@ -139,12 +139,12 @@ module TestSchema = isTypeOf = (fun o -> o :? Human), fieldsFn = fun () -> [ - Define.Field("id", String, "The id of the human.", fun _ (h : Human) -> h.Id) - Define.Field("name", Nullable String, "The name of the human.", fun _ (h : Human) -> h.Name) + Define.Field("id", StringType, "The id of the human.", fun _ (h : Human) -> h.Id) + Define.Field("name", Nullable StringType, "The name of the human.", fun _ (h : Human) -> h.Name) Define.Field("friends", ListOf (Nullable CharacterType), "The friends of the human, or an empty list if they have none.", fun _ (h : Human) -> h.Friends |> List.map getCharacter |> List.toSeq) Define.Field("appearsIn", ListOf EpisodeType, "Which movies they appear in.", fun _ (h : Human) -> h.AppearsIn) - Define.Field("homePlanet", Nullable String, "The home planet of the human, or null if unknown.", fun _ h -> h.HomePlanet) + Define.Field("homePlanet", Nullable StringType, "The home planet of the human, or null if unknown.", fun _ h -> h.HomePlanet) ]) and DroidType = @@ -154,12 +154,12 @@ module TestSchema = isTypeOf = (fun o -> o :? Droid), fieldsFn = fun () -> [ - Define.Field("id", String, "The id of the droid.", fun _ (d : Droid) -> d.Id) - Define.Field("name", Nullable String, "The name of the Droid.", fun _ (d : Droid) -> d.Name) + Define.Field("id", StringType, "The id of the droid.", fun _ (d : Droid) -> d.Id) + Define.Field("name", Nullable StringType, "The name of the Droid.", fun _ (d : Droid) -> d.Name) Define.Field("friends", ListOf (Nullable CharacterType), "The friends of the Droid, or an empty list if they have none.", fun _ (d : Droid) -> d.Friends |> List.map getCharacter |> List.toSeq) Define.Field("appearsIn", ListOf EpisodeType, "Which movies they appear in.", fun _ d -> d.AppearsIn) - Define.Field("primaryFunction", Nullable String, "The primary function of the droid.", fun _ d -> d.PrimaryFunction) + Define.Field("primaryFunction", Nullable StringType, "The primary function of the droid.", fun _ d -> d.PrimaryFunction) ]) and PlanetType = @@ -169,9 +169,9 @@ module TestSchema = isTypeOf = (fun o -> o :? Planet), fieldsFn = fun () -> [ - Define.Field("id", String, "The id of the planet", fun _ p -> p.Id) - Define.Field("name", Nullable String, "The name of the planet.", fun _ p -> p.Name) - Define.Field("isMoon", Nullable Boolean, "Is that a moon?", fun _ p -> p.IsMoon) + Define.Field("id", StringType, "The id of the planet", fun _ p -> p.Id) + Define.Field("name", Nullable StringType, "The name of the planet.", fun _ p -> p.Name) + Define.Field("isMoon", Nullable BooleanType, "Is that a moon?", fun _ p -> p.IsMoon) ]) and RootType = @@ -181,16 +181,16 @@ module TestSchema = isTypeOf = (fun o -> o :? Root), fieldsFn = fun () -> [ - Define.Field("requestId", String, "The ID of the client.", fun _ (r : Root) -> r.RequestId) + Define.Field("requestId", StringType, "The ID of the client.", fun _ (r : Root) -> r.RequestId) ]) let Query = Define.Object( name = "Query", fields = [ - Define.Field("hero", Nullable HumanType, "Gets human hero", [ Define.Input("id", String) ], fun ctx _ -> getHuman (ctx.Arg("id"))) - Define.Field("droid", Nullable DroidType, "Gets droid", [ Define.Input("id", String) ], fun ctx _ -> getDroid (ctx.Arg("id"))) - Define.Field("planet", Nullable PlanetType, "Gets planet", [ Define.Input("id", String) ], fun ctx _ -> getPlanet (ctx.Arg("id"))) + Define.Field("hero", Nullable HumanType, "Gets human hero", [ Define.Input("id", StringType) ], fun ctx _ -> getHuman (ctx.Arg("id"))) + Define.Field("droid", Nullable DroidType, "Gets droid", [ Define.Input("id", StringType) ], fun ctx _ -> getDroid (ctx.Arg("id"))) + Define.Field("planet", Nullable PlanetType, "Gets planet", [ Define.Input("id", StringType) ], fun ctx _ -> getPlanet (ctx.Arg("id"))) Define.Field("characters", ListOf CharacterType, "Gets characters", fun _ _ -> characters) ]) let Subscription = @@ -202,7 +202,7 @@ module TestSchema = RootType, PlanetType, "Watches to see if a planet is a moon.", - [ Define.Input("id", String) ], + [ Define.Input("id", StringType) ], (fun ctx _ p -> if ctx.Arg("id") = p.Id then Some p else None)) ]) let schemaConfig = SchemaConfig.Default @@ -215,7 +215,7 @@ module TestSchema = "setMoon", Nullable PlanetType, "Defines if a planet is actually a moon or not.", - [ Define.Input("id", String); Define.Input("isMoon", Boolean) ], + [ Define.Input("id", StringType); Define.Input("isMoon", BooleanType) ], fun ctx _ -> getPlanet (ctx.Arg("id")) |> Option.map (fun x -> From a616a9f19b1e1472b9fa9fb8421e23cd3d0b3419 Mon Sep 17 00:00:00 2001 From: valber Date: Sat, 24 Feb 2024 17:36:55 +0100 Subject: [PATCH 051/100] Replacing locally maintained "Rop" with FsToolkit.ErrorHandling in... in ...Server.AspNetCore --- ...harp.Data.GraphQL.Server.AspNetCore.fsproj | 4 +- .../Giraffe/HttpHandlers.fs | 208 ++++++++---------- .../Rop/Rop.fs | 139 ------------ .../Rop/RopAsync.fs | 20 -- .../Serialization/GraphQLQueryDecoding.fs | 124 ++++++----- .../Serialization/JsonConverters.fs | 30 +-- 6 files changed, 174 insertions(+), 351 deletions(-) delete mode 100644 src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/Rop.fs delete mode 100644 src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/RopAsync.fs diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj index 6d5c60b8f..1db5c33fd 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj @@ -12,8 +12,6 @@ - - @@ -30,6 +28,8 @@ + + diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 79f89ac3f..5b6547802 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -4,12 +4,13 @@ open FSharp.Data.GraphQL.Execution open FSharp.Data.GraphQL open Giraffe open FSharp.Data.GraphQL.Server.AspNetCore -open FSharp.Data.GraphQL.Server.AspNetCore.Rop +open FsToolkit.ErrorHandling open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging open System.Collections.Generic open System.Text.Json +open System.Text.Json.Serialization open System.Threading open System.Threading.Tasks @@ -46,135 +47,110 @@ module HttpHandlers = ] ) - let private addToErrorsInData (theseNew: GQLProblemDetails list) (data : IDictionary) = - let toNameValueLookupList (errors : GQLProblemDetails list) = - errors - |> List.map - (fun (error) -> - NameValueLookup.ofList - ["message", upcast error.Message - "path", upcast error.Path] - ) - let result = - data - |> Seq.map(fun x -> (x.Key, x.Value)) - |> Map.ofSeq - - if theseNew |> List.isEmpty then - result // because we want to avoid having an "errors" property as an empty list (according to specification) - else - match data.TryGetValue("errors") with - | (true, (:? list as nameValueLookups)) -> - result - |> Map.change - "errors" - (fun _ -> Some <| upcast (nameValueLookups @ (theseNew |> toNameValueLookupList) |> List.distinct)) - | (true, _) -> - result - | (false, _) -> - result - |> Map.add - "errors" - (upcast (theseNew |> toNameValueLookupList)) - let handleGraphQLWithResponseInterception<'Root> (cancellationToken : CancellationToken) (logger : ILogger) (interceptor : HttpHandler) (next : HttpFunc) (ctx : HttpContext) = task { - let cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ctx.RequestAborted).Token - if cancellationToken.IsCancellationRequested then - return (fun _ -> None) ctx - else - let options = ctx.RequestServices.GetRequiredService>() - let executor = options.SchemaExecutor - let rootFactory = options.RootFactory - let serializerOptions = options.SerializerOptions - let deserializeGraphQLRequest () = - try - JsonSerializer.DeserializeAsync( - ctx.Request.Body, - serializerOptions - ).AsTask() - .ContinueWith>(fun (x : Task) -> succeed x.Result) - with - | :? GraphQLException as ex -> - Task.FromResult(fail (sprintf "%s" (ex.Message))) + let cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ctx.RequestAborted).Token + if cancellationToken.IsCancellationRequested then + return (fun _ -> None) ctx + else + let options = ctx.RequestServices.GetRequiredService>() + let executor = options.SchemaExecutor + let rootFactory = options.RootFactory + let serializerOptions = options.SerializerOptions + let deserializeGraphQLRequest () = + task { + try + let! deserialized = + JsonSerializer.DeserializeAsync( + ctx.Request.Body, + serializerOptions + ) + return Ok deserialized + with + | :? GraphQLException as ex -> + return Result.Error [sprintf "%s" (ex.Message)] + } - let applyPlanExecutionResult (result : GQLExecutionResult) = - task { - let gqlResponse = - match result.Content with - | Direct (data, errs) -> - GQLResponse.Direct(result.DocumentId, data, errs) - | _ -> - GQLResponse.RequestError( - result.DocumentId, - [ GQLProblemDetails.Create( - "subscriptions are not supported here (use the websocket endpoint instead)." - )] - ) - return! httpOk cancellationToken interceptor serializerOptions gqlResponse next ctx - } + let applyPlanExecutionResult (result : GQLExecutionResult) = + task { + let gqlResponse = + match result.Content with + | Direct (data, errs) -> + GQLResponse.Direct(result.DocumentId, data, errs) + | _ -> + GQLResponse.RequestError( + result.DocumentId, + [ GQLProblemDetails.Create( + "subscriptions are not supported here (use the websocket endpoint instead)." + )] + ) + return! httpOk cancellationToken interceptor serializerOptions gqlResponse next ctx + } - let handleDeserializedGraphQLRequest (graphqlRequest : GraphQLRequest) = - task { - match graphqlRequest.Query with - | None -> - let! result = executor.AsyncExecute (IntrospectionQuery.Definition) |> Async.StartAsTask + let handleDeserializedGraphQLRequest (graphqlRequest : GraphQLRequest) = + task { + match graphqlRequest.Query with + | None -> + let! result = executor.AsyncExecute (IntrospectionQuery.Definition) |> Async.StartAsTask + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) + else + () + return! result |> applyPlanExecutionResult + | Some queryAsStr -> + let graphQLQueryDecodingResult = + queryAsStr + |> GraphQLQueryDecoding.decodeGraphQLQuery + serializerOptions + executor + graphqlRequest.OperationName + graphqlRequest.Variables + match graphQLQueryDecodingResult with + | Result.Error struct (docId, probDetails) -> + return! + httpOk cancellationToken interceptor serializerOptions (GQLResponse.RequestError (docId, probDetails)) next ctx + | Ok query -> + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(sprintf "Received query: %A" query) + else + () + let root = rootFactory(ctx) + let! result = + let variables = ImmutableDictionary.CreateRange( + query.Variables + |> Map.map (fun _ value -> value :?> JsonElement) + ) + executor.AsyncExecute( + query.ExecutionPlan, + data = root, + variables = variables + )|> Async.StartAsTask if logger.IsEnabled(LogLevel.Debug) then logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) else () return! result |> applyPlanExecutionResult - | Some queryAsStr -> - let graphQLQueryDecodingResult = - queryAsStr - |> GraphQLQueryDecoding.decodeGraphQLQuery - serializerOptions - executor - graphqlRequest.OperationName - graphqlRequest.Variables - match graphQLQueryDecodingResult with - | Failure errMsgs -> - return! - httpOk cancellationToken interceptor serializerOptions (prepareGenericErrors errMsgs) next ctx - | Success (query, _) -> - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(sprintf "Received query: %A" query) - else - () - let root = rootFactory(ctx) - let! result = - let variables = ImmutableDictionary.CreateRange( - query.Variables - |> Map.map (fun _ value -> value :?> JsonElement) - ) - executor.AsyncExecute( - query.ExecutionPlan, - data = root, - variables = variables - )|> Async.StartAsTask - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) - else - () - return! result |> applyPlanExecutionResult - } - if ctx.Request.Headers.ContentLength.GetValueOrDefault(0) = 0 then - let! result = executor.AsyncExecute (IntrospectionQuery.Definition) |> Async.StartAsTask - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) - else - () - return! result |> applyPlanExecutionResult + } + if ctx.Request.Headers.ContentLength.GetValueOrDefault(0) = 0 then + let! result = executor.AsyncExecute (IntrospectionQuery.Definition) |> Async.StartAsTask + if logger.IsEnabled(LogLevel.Debug) then + logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) else - match! deserializeGraphQLRequest() with - | Failure errMsgs -> - return! httpOk cancellationToken interceptor serializerOptions (prepareGenericErrors errMsgs) next ctx - | Success (graphqlRequest, _) -> - return! handleDeserializedGraphQLRequest graphqlRequest + () + return! result |> applyPlanExecutionResult + else + match! deserializeGraphQLRequest() with + | Result.Error errMsgs -> + let probDetails = + errMsgs + |> List.map (fun msg -> GQLProblemDetails.Create(msg, Skip)) + return! httpOk cancellationToken interceptor serializerOptions (GQLResponse.RequestError(-1, probDetails)) next ctx + | Ok graphqlRequest -> + return! handleDeserializedGraphQLRequest graphqlRequest } let handleGraphQL<'Root> diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/Rop.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/Rop.fs deleted file mode 100644 index 808ac0594..000000000 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/Rop.fs +++ /dev/null @@ -1,139 +0,0 @@ -// https://fsharpforfunandprofit.com/rop/ - -module FSharp.Data.GraphQL.Server.AspNetCore.Rop - -/// A Result is a success or failure -/// The Success case has a success value, plus a list of messages -/// The Failure case has just a list of messages -type RopResult<'TSuccess, 'TMessage> = - | Success of 'TSuccess * 'TMessage list - | Failure of 'TMessage list - -/// create a Success with no messages -let succeed x = - Success (x,[]) - -/// create a Success with a message -let succeedWithMsg x msg = - Success (x,[msg]) - -/// create a Failure with a message -let fail msg = - Failure [msg] - -/// A function that applies either fSuccess or fFailure -/// depending on the case. -let either fSuccess fFailure = function - | Success (x,msgs) -> fSuccess (x,msgs) - | Failure errors -> fFailure errors - -/// merge messages with a result -let mergeMessages msgs result = - let fSuccess (x,msgs2) = - Success (x, msgs @ msgs2) - let fFailure errs = - Failure (errs @ msgs) - either fSuccess fFailure result - -/// given a function that generates a new RopResult -/// apply it only if the result is on the Success branch -/// merge any existing messages with the new result -let bindR f result = - let fSuccess (x,msgs) = - f x |> mergeMessages msgs - let fFailure errs = - Failure errs - either fSuccess fFailure result - -/// given a function wrapped in a result -/// and a value wrapped in a result -/// apply the function to the value only if both are Success -let applyR f result = - match f,result with - | Success (f,msgs1), Success (x,msgs2) -> - (f x, msgs1@msgs2) |> Success - | Failure errs, Success (_,msgs) - | Success (_,msgs), Failure errs -> - errs @ msgs |> Failure - | Failure errs1, Failure errs2 -> - errs1 @ errs2 |> Failure - -/// infix version of apply -let (<*>) = applyR - -/// given a function that transforms a value -/// apply it only if the result is on the Success branch -let liftR f result = - let f' = f |> succeed - applyR f' result - -/// given two values wrapped in results apply a function to both -let lift2R f result1 result2 = - let f' = liftR f result1 - applyR f' result2 - -/// given three values wrapped in results apply a function to all -let lift3R f result1 result2 result3 = - let f' = lift2R f result1 result2 - applyR f' result3 - -/// given four values wrapped in results apply a function to all -let lift4R f result1 result2 result3 result4 = - let f' = lift3R f result1 result2 result3 - applyR f' result4 - -/// infix version of liftR -let () = liftR - -/// synonym for liftR -let mapR = liftR - -/// given an RopResult, call a unit function on the success branch -/// and pass thru the result -let successTee f result = - let fSuccess (x,msgs) = - f (x,msgs) - Success (x,msgs) - let fFailure errs = Failure errs - either fSuccess fFailure result - -/// given an RopResult, call a unit function on the failure branch -/// and pass thru the result -let failureTee f result = - let fSuccess (x,msgs) = Success (x,msgs) - let fFailure errs = - f errs - Failure errs - either fSuccess fFailure result - -/// given an RopResult, map the messages to a different error type -let mapMessagesR f result = - match result with - | Success (x,msgs) -> - let msgs' = List.map f msgs - Success (x, msgs') - | Failure errors -> - let errors' = List.map f errors - Failure errors' - -/// given an RopResult, in the success case, return the value. -/// In the failure case, determine the value to return by -/// applying a function to the errors in the failure case -let valueOrDefault f result = - match result with - | Success (x,_) -> x - | Failure errors -> f errors - -/// lift an option to a RopResult. -/// Return Success if Some -/// or the given message if None -let failIfNone message = function - | Some x -> succeed x - | None -> fail message - -/// given an RopResult option, return it -/// or the given message if None -let failIfNoneR message = function - | Some rop -> rop - | None -> fail message - diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/RopAsync.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/RopAsync.fs deleted file mode 100644 index 960fa77b7..000000000 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Rop/RopAsync.fs +++ /dev/null @@ -1,20 +0,0 @@ -module FSharp.Data.GraphQL.Server.AspNetCore.RopAsync - -open Rop -open System.Threading.Tasks - -let bindToAsyncTaskR<'a, 'b, 'c> (f: 'a -> Task>) result = - task { - let secondResult = - match result with - | Success (x, msgs) -> - task { - let! secRes = f x - return secRes |> mergeMessages msgs - } - | Failure errs -> - task { - return Failure errs - } - return! secondResult - } \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs index 281ad3a2c..9f64cba2a 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs @@ -2,83 +2,87 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore module GraphQLQueryDecoding = open FSharp.Data.GraphQL - open Rop open System open System.Text.Json open System.Text.Json.Serialization + open FsToolkit.ErrorHandling + + let genericErrorContentForDoc (docId: int) (message: string) = + struct (docId, [GQLProblemDetails.Create ((sprintf "%s" message), Skip)]) + + let genericFinalErrorForDoc (docId: int) (message: string) = + Result.Error (genericErrorContentForDoc docId message) + + let genericFinalError message = + message |> genericFinalErrorForDoc -1 let private resolveVariables (serializerOptions : JsonSerializerOptions) (expectedVariables : Types.VarDef list) (variableValuesObj : JsonDocument) = - try + result { try - if (not (variableValuesObj.RootElement.ValueKind.Equals(JsonValueKind.Object))) then - let offendingValueKind = variableValuesObj.RootElement.ValueKind - fail (sprintf "\"variables\" must be an object, but here it is \"%A\" instead" offendingValueKind) - else - let providedVariableValues = variableValuesObj.RootElement.EnumerateObject() |> List.ofSeq - expectedVariables - |> List.choose - (fun expectedVariable -> - providedVariableValues - |> List.tryFind(fun x -> x.Name = expectedVariable.Name) - |> Option.map - (fun providedValue -> - let boxedValue = - if providedValue.Value.ValueKind.Equals(JsonValueKind.Null) then - null :> obj - elif providedValue.Value.ValueKind.Equals(JsonValueKind.String) then - providedValue.Value.GetString() :> obj - else - JsonSerializer.Deserialize(providedValue.Value, serializerOptions) :> obj - (expectedVariable.Name, boxedValue) + try + if (not (variableValuesObj.RootElement.ValueKind.Equals(JsonValueKind.Object))) then + let offendingValueKind = variableValuesObj.RootElement.ValueKind + return! Result.Error (sprintf "\"variables\" must be an object, but here it is \"%A\" instead" offendingValueKind) + else + let providedVariableValues = variableValuesObj.RootElement.EnumerateObject() |> List.ofSeq + return! Ok + (expectedVariables + |> List.choose + (fun expectedVariable -> + providedVariableValues + |> List.tryFind(fun x -> x.Name = expectedVariable.Name) + |> Option.map + (fun providedValue -> + let boxedValue = + if providedValue.Value.ValueKind.Equals(JsonValueKind.Null) then + null :> obj + elif providedValue.Value.ValueKind.Equals(JsonValueKind.String) then + providedValue.Value.GetString() :> obj + else + JsonSerializer.Deserialize(providedValue.Value, serializerOptions) :> obj + (expectedVariable.Name, boxedValue) + ) ) - ) - |> Map.ofList - |> succeed - with - | :? JsonException as ex -> - fail (sprintf "%s" (ex.Message)) - | :? GraphQLException as ex -> - fail (sprintf "%s" (ex.Message)) - | ex -> - printfn "%s" (ex.ToString()) - fail "Something unexpected happened during the parsing of this request." - finally - variableValuesObj.Dispose() + |> Map.ofList) + with + | :? JsonException as ex -> + return! Result.Error (sprintf "%s" (ex.Message)) + | :? GraphQLException as ex -> + return! Result.Error (sprintf "%s" (ex.Message)) + | ex -> + printfn "%s" (ex.ToString()) + return! Result.Error ("Something unexpected happened during the parsing of this request.") + finally + variableValuesObj.Dispose() + } let decodeGraphQLQuery (serializerOptions : JsonSerializerOptions) (executor : Executor<'a>) (operationName : string option) (variables : JsonDocument option) (query : string) = let executionPlanResult = - try - match operationName with - | Some operationName -> - executor.CreateExecutionPlan(query, operationName = operationName) - |> succeed - | None -> - executor.CreateExecutionPlan(query) - |> succeed - with - | :? JsonException as ex -> - fail (sprintf "%s" (ex.Message)) - | :? GraphQLException as ex -> - fail (sprintf "%s" (ex.Message)) + result { + try + match operationName with + | Some operationName -> + return! executor.CreateExecutionPlan(query, operationName = operationName) + | None -> + return! executor.CreateExecutionPlan(query) + with + | :? JsonException as ex -> + return! genericFinalError (sprintf "%s" (ex.Message)) + | :? GraphQLException as ex -> + return! genericFinalError (sprintf "%s" (ex.Message)) + } executionPlanResult - |> bindR - (function - | Ok x -> Success (x, []) - | Result.Error (_, problemDetails) -> - Failure <| - (problemDetails - |> List.map (fun (x: GQLProblemDetails) -> x.Message)) - ) - |> bindR + |> Result.bind (fun executionPlan -> match variables with - | None -> succeed <| (executionPlan, Map.empty) // it's none of our business here if some variables are expected. If that's the case, execution of the ExecutionPlan will take care of that later (and issue an error). + | None -> Ok <| (executionPlan, Map.empty) // it's none of our business here if some variables are expected. If that's the case, execution of the ExecutionPlan will take care of that later (and issue an error). | Some variableValuesObj -> variableValuesObj |> resolveVariables serializerOptions executionPlan.Variables - |> mapR (fun variableValsObj -> (executionPlan, variableValsObj)) + |> Result.map (fun variableValsObj -> (executionPlan, variableValsObj)) + |> Result.mapError (fun x -> sprintf "%s" x |> genericErrorContentForDoc executionPlan.DocumentId) ) - |> mapR (fun (executionPlan, variables) -> + |> Result.map (fun (executionPlan, variables) -> { ExecutionPlan = executionPlan Variables = variables }) \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index 705252c92..d8bc18035 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -1,7 +1,7 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL -open Rop +// open Rop open System open System.Text.Json open System.Text.Json.Serialization @@ -17,14 +17,16 @@ type ClientMessageConverter<'Root>(executor : Executor<'Root>) = /// The <error-message> can be vaguely descriptive on why the received message is invalid." let invalidMsg (explanation : string) = InvalidMessage (4400, explanation) - |> fail + |> Result.Error + + let errMsgToStr (struct (docId: int, graphQLErrorMsgs: GQLProblemDetails list)) = + String.Join('\n', graphQLErrorMsgs |> Seq.map (fun err -> err.Message)) let unpackRopResult ropResult = match ropResult with - | Success (x, _) -> x - | Failure (failures : ClientMessageProtocolFailure list) -> - System.String.Join("\n\n", (failures |> Seq.map (fun (InvalidMessage (_, explanation)) -> explanation))) - |> raiseInvalidMsg + | Ok x -> x + | Result.Error (InvalidMessage (_, explanation: string)) -> + raiseInvalidMsg explanation let getOptionalString (reader : byref) = if reader.TokenType.Equals(JsonTokenType.Null) then @@ -38,12 +40,12 @@ type ClientMessageConverter<'Root>(executor : Executor<'Root>) = else raiseInvalidMsg <| sprintf "was expecting a value for property \"%s\"" propertyName - let requireId (raw : RawMessage) : RopResult = + let requireId (raw : RawMessage) : Result = match raw.Id with - | Some s -> succeed s + | Some s -> Ok s | None -> invalidMsg <| "property \"id\" is required for this message but was not present." - let requireSubscribePayload (serializerOptions : JsonSerializerOptions) (executor : Executor<'a>) (payload : JsonDocument option) : RopResult = + let requireSubscribePayload (serializerOptions : JsonSerializerOptions) (executor : Executor<'a>) (payload : JsonDocument option) : Result = match payload with | None -> invalidMsg <| "payload is required for this message, but none was present." @@ -59,7 +61,7 @@ type ClientMessageConverter<'Root>(executor : Executor<'Root>) = | Some query -> query |> GraphQLQueryDecoding.decodeGraphQLQuery serializerOptions executor subscribePayload.OperationName subscribePayload.Variables - |> mapMessagesR (fun errMsg -> InvalidMessage (CustomWebSocketStatus.invalidMessage, errMsg)) + |> Result.mapError (fun errMsg -> InvalidMessage (CustomWebSocketStatus.invalidMessage, errMsg |> errMsgToStr)) let readRawMessage (reader : byref, options: JsonSerializerOptions) : RawMessage = @@ -100,18 +102,18 @@ type ClientMessageConverter<'Root>(executor : Executor<'Root>) = | "complete" -> raw |> requireId - |> mapR ClientComplete + |> Result.map ClientComplete |> unpackRopResult | "subscribe" -> raw |> requireId - |> bindR + |> Result.bind (fun id -> raw.Payload |> requireSubscribePayload options executor - |> mapR (fun payload -> (id, payload)) + |> Result.map (fun payload -> (id, payload)) ) - |> mapR Subscribe + |> Result.map Subscribe |> unpackRopResult | other -> raiseInvalidMsg <| sprintf "invalid type \"%s\" specified by client." other From c28251bfa226f4abf8a1d0b896ecf7c6e8adf1f0 Mon Sep 17 00:00:00 2001 From: valber Date: Sat, 24 Feb 2024 17:43:55 +0100 Subject: [PATCH 052/100] Complement to my last commit (removed commented out code) --- .../Serialization/JsonConverters.fs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index d8bc18035..4a876e14e 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -1,7 +1,6 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL -// open Rop open System open System.Text.Json open System.Text.Json.Serialization From 9e4f942684cea3980a4f179e638a971ac3ac2167 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 25 Feb 2024 22:05:29 +0100 Subject: [PATCH 053/100] Also in GraphQLWebsocketMiddleware using FsToolkit.ErrorHandling... ... instead of locally maintained (and now removed) Rop solution. --- .../GraphQLWebsocketMiddleware.fs | 125 +++++++++--------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 27de87302..bb2879f60 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -2,7 +2,6 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL open Microsoft.AspNetCore.Http -open FSharp.Data.GraphQL.Server.AspNetCore.Rop open System open System.Collections.Generic open System.Net.WebSockets @@ -10,6 +9,7 @@ open System.Text.Json open System.Threading open System.Threading.Tasks open FSharp.Data.GraphQL.Execution +open FsToolkit.ErrorHandling open Microsoft.Extensions.Hosting open Microsoft.Extensions.Logging open System.Collections.Immutable @@ -48,22 +48,20 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti } let deserializeClientMessage (serializerOptions : JsonSerializerOptions) (msg: string) = - task { + taskResult { try - return - JsonSerializer.Deserialize(msg, serializerOptions) - |> succeed + return JsonSerializer.Deserialize(msg, serializerOptions) with | :? InvalidMessageException as e -> - return - fail <| InvalidMessage(4400, e.Message.ToString()) + return! Result.Error <| + InvalidMessage(4400, e.Message.ToString()) | :? JsonException as e -> if logger.IsEnabled(LogLevel.Debug) then logger.LogDebug(e.ToString()) else () - return - fail <| InvalidMessage(4400, "invalid json in client message") + return! Result.Error <| + InvalidMessage(4400, "invalid json in client message") } let isSocketOpen (theSocket : WebSocket) = @@ -75,8 +73,8 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti not (theSocket.State = WebSocketState.Aborted) && not (theSocket.State = WebSocketState.Closed) - let receiveMessageViaSocket (cancellationToken : CancellationToken) (serializerOptions: JsonSerializerOptions) (socket : WebSocket) : Task option> = - task { + let receiveMessageViaSocket (cancellationToken : CancellationToken) (serializerOptions: JsonSerializerOptions) (socket : WebSocket) = + taskResult { let buffer = Array.zeroCreate 4096 let completeMessage = new List() let mutable segmentResponse : WebSocketReceiveResult = null @@ -261,48 +259,50 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti while not cancellationToken.IsCancellationRequested && socket |> isSocketOpen do let! receivedMessage = rcv() match receivedMessage with - | None -> - logger.LogTrace("Websocket socket received empty message! (socket state = {socketstate})", socket.State) - | Some msg -> - match msg with - | Failure failureMsgs -> - "InvalidMessage" |> logMsgReceivedWithOptionalPayload None - match failureMsgs |> List.head with - | InvalidMessage (code, explanation) -> - do! socket.CloseAsync(enum code, explanation, CancellationToken.None) - | Success (ConnectionInit p, _) -> - "ConnectionInit" |> logMsgReceivedWithOptionalPayload p - do! socket.CloseAsync( - enum CustomWebSocketStatus.tooManyInitializationRequests, - "too many initialization requests", - CancellationToken.None) - | Success (ClientPing p, _) -> - "ClientPing" |> logMsgReceivedWithOptionalPayload p - match pingHandler with - | Some func -> - let! customP = p |> func serviceProvider - do! ServerPong customP |> sendMsg - | None -> - do! ServerPong p |> sendMsg - | Success (ClientPong p, _) -> - "ClientPong" |> logMsgReceivedWithOptionalPayload p - | Success (Subscribe (id, query), _) -> - "Subscribe" |> logMsgWithIdReceived id - if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then + | Result.Error failureMsgs -> + "InvalidMessage" |> logMsgReceivedWithOptionalPayload None + match failureMsgs with + | InvalidMessage (code, explanation) -> + do! socket.CloseAsync(enum code, explanation, CancellationToken.None) + | Ok maybeMsg -> + match maybeMsg with + | None -> + logger.LogTrace("Websocket socket received empty message! (socket state = {socketstate})", socket.State) + | Some msg -> + match msg with + | ConnectionInit p -> + "ConnectionInit" |> logMsgReceivedWithOptionalPayload p do! socket.CloseAsync( - enum CustomWebSocketStatus.subscriberAlreadyExists, - sprintf "Subscriber for %s already exists" id, + enum CustomWebSocketStatus.tooManyInitializationRequests, + "too many initialization requests", CancellationToken.None) - else - let variables = ImmutableDictionary.CreateRange(query.Variables |> Map.map (fun _ value -> value :?> JsonElement)) - let! planExecutionResult = - executor.AsyncExecute(query.ExecutionPlan, root(httpContext), variables) - |> Async.StartAsTask - do! planExecutionResult - |> applyPlanExecutionResult id socket - | Success (ClientComplete id, _) -> - "ClientComplete" |> logMsgWithIdReceived id - subscriptions |> GraphQLSubscriptionsManagement.removeSubscription (id) + | ClientPing p -> + "ClientPing" |> logMsgReceivedWithOptionalPayload p + match pingHandler with + | Some func -> + let! customP = p |> func serviceProvider + do! ServerPong customP |> sendMsg + | None -> + do! ServerPong p |> sendMsg + | ClientPong p -> + "ClientPong" |> logMsgReceivedWithOptionalPayload p + | Subscribe (id, query) -> + "Subscribe" |> logMsgWithIdReceived id + if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then + do! socket.CloseAsync( + enum CustomWebSocketStatus.subscriberAlreadyExists, + sprintf "Subscriber for %s already exists" id, + CancellationToken.None) + else + let variables = ImmutableDictionary.CreateRange(query.Variables |> Map.map (fun _ value -> value :?> JsonElement)) + let! planExecutionResult = + executor.AsyncExecute(query.ExecutionPlan, root(httpContext), variables) + |> Async.StartAsTask + do! planExecutionResult + |> applyPlanExecutionResult id socket + | ClientComplete id -> + "ClientComplete" |> logMsgWithIdReceived id + subscriptions |> GraphQLSubscriptionsManagement.removeSubscription (id) logger.LogTrace "Leaving graphql-ws connection loop..." do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior with @@ -318,8 +318,8 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti // <-- Main // <-------- - let waitForConnectionInitAndRespondToClient (serializerOptions : JsonSerializerOptions) (connectionInitTimeoutInMs : int) (socket : WebSocket) : Task> = - task { + let waitForConnectionInitAndRespondToClient (serializerOptions : JsonSerializerOptions) (connectionInitTimeoutInMs : int) (socket : WebSocket) : TaskResult = + taskResult { let timerTokenSource = new CancellationTokenSource() timerTokenSource.CancelAfter(connectionInitTimeoutInMs) let detonationRegistration = timerTokenSource.Token.Register(fun _ -> @@ -327,22 +327,23 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.connectionTimeout, "Connection initialization timeout") |> Task.WaitAll ) - let! connectionInitSucceeded = Task.Run((fun _ -> + + let! connectionInitSucceeded = TaskResult.Run((fun _ -> task { logger.LogDebug("Waiting for ConnectionInit...") let! receivedMessage = receiveMessageViaSocket (CancellationToken.None) serializerOptions socket match receivedMessage with - | Some (Success (ConnectionInit payload, _)) -> + | Ok (Some (ConnectionInit _)) -> logger.LogDebug("Valid connection_init received! Responding with ACK!") detonationRegistration.Unregister() |> ignore do! ConnectionAck |> sendMessageViaSocket serializerOptions socket return true - | Some (Success (Subscribe _, _)) -> + | Ok (Some (Subscribe _)) -> do! socket |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.unauthorized, "Unauthorized") return false - | Some (Failure [InvalidMessage (code, explanation)]) -> + | Result.Error (InvalidMessage (code, explanation)) -> do! socket |> tryToGracefullyCloseSocket (enum code, explanation) @@ -355,11 +356,11 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti }), timerTokenSource.Token) if (not timerTokenSource.Token.IsCancellationRequested) then if connectionInitSucceeded then - return (succeed ()) + return () else - return (fail "ConnectionInit failed (not because of timeout)") + return! Result.Error <| "ConnectionInit failed (not because of timeout)" else - return (fail "ConnectionInit timeout") + return! Result.Error <| "ConnectionInit timeout" } member __.InvokeAsync(ctx : HttpContext) = @@ -373,9 +374,9 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti socket |> waitForConnectionInitAndRespondToClient options.SerializerOptions options.WebsocketOptions.ConnectionInitTimeoutInMs match connectionInitResult with - | Failure errMsg -> + | Result.Error errMsg -> logger.LogWarning("{warningmsg}", (sprintf "%A" errMsg)) - | Success _ -> + | Ok _ -> let longRunningCancellationToken = (CancellationTokenSource.CreateLinkedTokenSource(ctx.RequestAborted, applicationLifetime.ApplicationStopping).Token) longRunningCancellationToken.Register(fun _ -> From 566c8487b18443dd739d3e6d17272607e9a0485a Mon Sep 17 00:00:00 2001 From: valber Date: Mon, 26 Feb 2024 06:18:42 +0100 Subject: [PATCH 054/100] new HttpHandler: fixing error handling on "applyPlanExecutionResult" --- .../Giraffe/HttpHandlers.fs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 5b6547802..579e3c093 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -81,13 +81,18 @@ module HttpHandlers = match result.Content with | Direct (data, errs) -> GQLResponse.Direct(result.DocumentId, data, errs) + | RequestError (problemDetailsList) -> + GQLResponse.RequestError( + result.DocumentId, + problemDetailsList + ) | _ -> - GQLResponse.RequestError( - result.DocumentId, - [ GQLProblemDetails.Create( - "subscriptions are not supported here (use the websocket endpoint instead)." - )] - ) + GQLResponse.RequestError( + result.DocumentId, + [ GQLProblemDetails.Create( + "subscriptions are not supported here (use the websocket endpoint instead)." + )] + ) return! httpOk cancellationToken interceptor serializerOptions gqlResponse next ctx } From 00c1a29d72006829eb82de17ad556e3897ffd4ef Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 9 Feb 2024 18:31:12 +0400 Subject: [PATCH 055/100] Extracted the target frameworks into a single file --- Directory.Build.props | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 829f6d106..5087e944d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,10 +26,12 @@ https://fsprojects.github.io/FSharp.Data.GraphQL false MIT + true true snupkg - true true + + v From ce0d4375c17f9a1b24cc6fdb66b7bbea3fc190eb Mon Sep 17 00:00:00 2001 From: valber Date: Mon, 26 Feb 2024 23:00:28 +0100 Subject: [PATCH 056/100] .Server.AspNetCore: adding either package ref or proj. ref. according... ... according to property $(IsNuGet) --- .../FSharp.Data.GraphQL.Server.AspNetCore.fsproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj index 1db5c33fd..89c8efa99 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj @@ -24,7 +24,8 @@ - + + From 18a71d5926dcfc437a6314dccd9f09b1b4446ce3 Mon Sep 17 00:00:00 2001 From: valber Date: Mon, 26 Feb 2024 23:19:43 +0100 Subject: [PATCH 057/100] .Server.AspNetCore: added targets "pack" and "push" for this new project --- build/Program.fs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build/Program.fs b/build/Program.fs index 7592c2137..833ce9737 100644 --- a/build/Program.fs +++ b/build/Program.fs @@ -299,6 +299,9 @@ Target.create PackSharedTarget <| fun _ -> pack "Shared" let [] PackServerTarget = "PackServer" Target.create PackServerTarget <| fun _ -> pack "Server" +let [] PackServerAspNetCore = "PackServerAspNetCore" +Target.create "PackServerAspNetCore" <| fun _ -> pack "Server.AspNetCore" + let [] PackClientTarget = "PackClient" Target.create PackClientTarget <| fun _ -> pack "Client" @@ -314,6 +317,9 @@ Target.create PushSharedTarget <| fun _ -> push "Shared" let [] PushServerTarget = "PushServer" Target.create PushServerTarget <| fun _ -> push "Server" +let [] PushServerAspNetCore = "PushServerAspNetCore" +Target.create "PushServerAspNetCore" <| fun _ -> push "Server.AspNetCore" + let [] PushClientTarget = "PushClient" Target.create PushClientTarget <| fun _ -> push "Client" @@ -352,6 +358,8 @@ PackSharedTarget ==> PushClientTarget ==> PackServerTarget ==> PushServerTarget + ==> PackServerAspNetCore + ==> PushServerAspNetCore ==> PackMiddlewareTarget ==> PushMiddlewareTarget ==> PackRelayTarget From b95ebf2c85da8a48d70e4ea54d2a8645ba2bfd65 Mon Sep 17 00:00:00 2001 From: valber Date: Mon, 26 Feb 2024 23:27:34 +0100 Subject: [PATCH 058/100] .Server.AspNetCore: added some documentation to functions. --- .../Giraffe/HttpHandlers.fs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 579e3c093..a6b5157c1 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -47,6 +47,10 @@ module HttpHandlers = ] ) + /// HttpHandler for handling GraphQL requests with Giraffe. + /// This one is for specifying an interceptor when you need to + /// do a custom handling on the response. For example, when you want + /// to add custom headers. let handleGraphQLWithResponseInterception<'Root> (cancellationToken : CancellationToken) (logger : ILogger) @@ -158,6 +162,7 @@ module HttpHandlers = return! handleDeserializedGraphQLRequest graphqlRequest } + /// HttpHandler for handling GraphQL requests with Giraffe let handleGraphQL<'Root> (cancellationToken : CancellationToken) (logger : ILogger) From f6dd8280d3ca7cb8c192ea2607112d365108d180 Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 28 Feb 2024 04:00:07 +0100 Subject: [PATCH 059/100] .Server.AspNetCore: correctly parsing variable value to JsonElement --- .../Giraffe/HttpHandlers.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index a6b5157c1..1a6d5639c 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -131,7 +131,7 @@ module HttpHandlers = let! result = let variables = ImmutableDictionary.CreateRange( query.Variables - |> Map.map (fun _ value -> value :?> JsonElement) + |> Map.map (fun _ value -> JsonSerializer.SerializeToElement(value)) ) executor.AsyncExecute( query.ExecutionPlan, From 7216ece4530e48c9f017f87eae328e9de9db1646 Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 28 Feb 2024 04:53:00 +0100 Subject: [PATCH 060/100] chat-app sample: adapted code to new error handling and added proj to .. ... to solution FSharp.Data.GraphQL.sln --- FSharp.Data.GraphQL.sln | 19 +- samples/chat-app/server/Exceptions.fs | 24 +- ...FSharp.Data.GraphQL.Samples.ChatApp.fsproj | 4 + samples/chat-app/server/Program.fs | 5 +- samples/chat-app/server/Schema.fs | 205 +++++++++--------- 5 files changed, 139 insertions(+), 118 deletions(-) diff --git a/FSharp.Data.GraphQL.sln b/FSharp.Data.GraphQL.sln index 5f3f1f3f7..4b47d839b 100644 --- a/FSharp.Data.GraphQL.sln +++ b/FSharp.Data.GraphQL.sln @@ -170,6 +170,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "components", "components", samples\relay-modern-starter-kit\src\components\user.jsx = samples\relay-modern-starter-kit\src\components\user.jsx EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "chat-app", "chat-app", "{24AB1F5A-4996-4DDA-87E0-B82B3A24C13F}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.GraphQL.Samples.ChatApp", "samples\chat-app\server\FSharp.Data.GraphQL.Samples.ChatApp.fsproj", "{225B0790-C6B6-425C-9093-F359A4C635D3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -324,6 +328,18 @@ Global {A6A162DF-9FBB-4C2A-913F-FD5FED35A09B}.Release|x64.Build.0 = Release|Any CPU {A6A162DF-9FBB-4C2A-913F-FD5FED35A09B}.Release|x86.ActiveCfg = Release|Any CPU {A6A162DF-9FBB-4C2A-913F-FD5FED35A09B}.Release|x86.Build.0 = Release|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Debug|x64.ActiveCfg = Debug|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Debug|x64.Build.0 = Debug|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Debug|x86.ActiveCfg = Debug|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Debug|x86.Build.0 = Debug|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Release|Any CPU.Build.0 = Release|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Release|x64.ActiveCfg = Release|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Release|x64.Build.0 = Release|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Release|x86.ActiveCfg = Release|Any CPU + {225B0790-C6B6-425C-9093-F359A4C635D3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -352,8 +368,9 @@ Global {A8F031E0-2BD5-4BAE-830A-60CBA76A047D} = {600D4BE2-FCE0-4684-AC6F-2DC829B395BA} {6EEA0E79-693F-4D4F-B55B-DB0C64EBDA45} = {600D4BE2-FCE0-4684-AC6F-2DC829B395BA} {7AA3516E-60F5-4969-878F-4E3DCF3E63A3} = {A8F031E0-2BD5-4BAE-830A-60CBA76A047D} - {E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78} {554A6833-1E72-41B4-AAC1-C19371EC061B} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78} + {24AB1F5A-4996-4DDA-87E0-B82B3A24C13F} = {B0C25450-74BF-40C2-9E02-09AADBAE2C2F} + {225B0790-C6B6-425C-9093-F359A4C635D3} = {24AB1F5A-4996-4DDA-87E0-B82B3A24C13F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C5B9895C-9DF8-4557-8D44-7D0C4C31F86E} diff --git a/samples/chat-app/server/Exceptions.fs b/samples/chat-app/server/Exceptions.fs index 54c9217eb..9ca8e6c0a 100644 --- a/samples/chat-app/server/Exceptions.fs +++ b/samples/chat-app/server/Exceptions.fs @@ -2,24 +2,24 @@ module FSharp.Data.GraphQL.Samples.ChatApp.Exceptions open FSharp.Data.GraphQL -let Member_With_This_Name_Already_Exists (theName : string) = - GraphQLException(sprintf "member with name \"%s\" already exists" theName) +let Member_With_This_Name_Already_Exists (theName : string) : GraphQLException = + GQLMessageException(sprintf "member with name \"%s\" already exists" theName) -let Organization_Doesnt_Exist (theId : OrganizationId) = +let Organization_Doesnt_Exist (theId : OrganizationId) : GraphQLException = match theId with | OrganizationId x -> - GraphQLException (sprintf "organization with ID \"%s\" doesn't exist" (x.ToString())) + GQLMessageException (sprintf "organization with ID \"%s\" doesn't exist" (x.ToString())) -let ChatRoom_Doesnt_Exist (theId : ChatRoomId) = - GraphQLException(sprintf "chat room with ID \"%s\" doesn't exist" (theId.ToString())) +let ChatRoom_Doesnt_Exist (theId : ChatRoomId) : GraphQLException = + GQLMessageException(sprintf "chat room with ID \"%s\" doesn't exist" (theId.ToString())) -let PrivMember_Doesnt_Exist (theId : MemberPrivateId) = +let PrivMember_Doesnt_Exist (theId : MemberPrivateId) : GraphQLException = match theId with | MemberPrivateId x -> - GraphQLException(sprintf "member with private ID \"%s\" doesn't exist" (x.ToString())) + GQLMessageException(sprintf "member with private ID \"%s\" doesn't exist" (x.ToString())) -let Member_Isnt_Part_Of_Org () = - GraphQLException("this member is not part of this organization") +let Member_Isnt_Part_Of_Org () : GraphQLException = + GQLMessageException("this member is not part of this organization") -let ChatRoom_Isnt_Part_Of_Org () = - GraphQLException("this chat room is not part of this organization") \ No newline at end of file +let ChatRoom_Isnt_Part_Of_Org () : GraphQLException = + GQLMessageException("this chat room is not part of this organization") \ No newline at end of file diff --git a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj index c7e2b9a29..86c795c85 100644 --- a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj +++ b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj @@ -5,6 +5,10 @@ FSharp.Data.GraphQL.Samples.ChatApp + + + + diff --git a/samples/chat-app/server/Program.fs b/samples/chat-app/server/Program.fs index 1161dbe11..cfdb5a8a5 100644 --- a/samples/chat-app/server/Program.fs +++ b/samples/chat-app/server/Program.fs @@ -5,6 +5,7 @@ open FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe open System open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Server.Kestrel.Core open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Hosting @@ -12,8 +13,8 @@ open Microsoft.Extensions.Logging module Program = - let rootFactory () : Root = - { RequestId = Guid.NewGuid().ToString() } + let rootFactory (ctx : HttpContext) : Root = + { RequestId = ctx.TraceIdentifier } let errorHandler (ex : Exception) (log : ILogger) = log.LogError(EventId(), ex, "An unhandled exception has occurred while executing this request.") diff --git a/samples/chat-app/server/Schema.fs b/samples/chat-app/server/Schema.fs index 313267ed6..cbc7654d1 100644 --- a/samples/chat-app/server/Schema.fs +++ b/samples/chat-app/server/Schema.fs @@ -2,6 +2,7 @@ namespace FSharp.Data.GraphQL.Samples.ChatApp open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types +open FsToolkit.ErrorHandling open System type Root = @@ -76,46 +77,44 @@ module MapFrom = module Schema = - open FSharp.Data.GraphQL.Server.AspNetCore.Rop - let authenticateMemberInOrganization (organizationId : OrganizationId) (memberPrivId : MemberPrivateId) : RopResult<(Organization_In_Db * Member_In_Db), GraphQLException> = + let authenticateMemberInOrganization (organizationId : OrganizationId) (memberPrivId : MemberPrivateId) : Result<(Organization_In_Db * Member_In_Db), GraphQLException> = let maybeOrganization = FakePersistence.Organizations |> Map.tryFind organizationId let maybeMember = FakePersistence.Members.Values |> Seq.tryFind (fun x -> x.PrivId = memberPrivId) match (maybeOrganization, maybeMember) with | None, _ -> - fail <| (organizationId |> Exceptions.Organization_Doesnt_Exist) + Error (organizationId |> Exceptions.Organization_Doesnt_Exist) | _, None -> - fail <| (memberPrivId |> Exceptions.PrivMember_Doesnt_Exist) + Error (memberPrivId |> Exceptions.PrivMember_Doesnt_Exist) | Some organization, Some theMember -> - if not (organization.Members |> List.contains theMember.Id) then - fail <| (Exceptions.Member_Isnt_Part_Of_Org()) - else - succeed (organization, theMember) + if not (organization.Members |> List.contains theMember.Id) then + Error (Exceptions.Member_Isnt_Part_Of_Org()) + else + Ok (organization, theMember) - let validateChatRoomExistence (organization : Organization_In_Db) (chatRoomId : ChatRoomId) : RopResult = + let validateChatRoomExistence (organization : Organization_In_Db) (chatRoomId : ChatRoomId) : Result = match FakePersistence.ChatRooms |> Map.tryFind chatRoomId with | None -> - fail <| Exceptions.ChatRoom_Doesnt_Exist chatRoomId + Error <| Exceptions.ChatRoom_Doesnt_Exist chatRoomId | Some chatRoom -> - if not (organization.ChatRooms |> List.contains chatRoom.Id) then - fail <| Exceptions.ChatRoom_Isnt_Part_Of_Org() - else - succeed <| chatRoom + if not (organization.ChatRooms |> List.contains chatRoom.Id) then + Error <| Exceptions.ChatRoom_Isnt_Part_Of_Org() + else + Ok chatRoom - let validateMessageExistence (chatRoom : ChatRoom_In_Db) (messageId : MessageId) : RopResult = + let validateMessageExistence (chatRoom : ChatRoom_In_Db) (messageId : MessageId) : Result = match FakePersistence.ChatRoomMessages |> Map.tryFind (chatRoom.Id, messageId) with | None -> - fail (GraphQLException("chat message doesn't exist (anymore)")) + Error (GQLMessageException("chat message doesn't exist (anymore)")) | Some chatMessage -> - succeed chatMessage - - let succeedOrRaiseGraphQLEx<'T> (ropResult : RopResult<'T, GraphQLException>) : 'T = - match ropResult with - | Failure exs -> - let firstEx = exs |> List.head - raise firstEx - | Success (s, _) -> + Ok chatMessage + + let succeedOrRaiseGraphQLEx<'T> (result : Result<'T, GraphQLException>) : 'T = + match result with + | Error ex -> + raise ex + | Ok s -> s let chatRoomEvents_subscription_name = "chatRoomEvents" @@ -135,8 +134,8 @@ module Schema = description = "An organization member", isTypeOf = (fun o -> o :? Member), fieldsFn = fun () -> [ - Define.Field("id", SchemaDefinitions.Guid, "the member's ID", fun _ (x : Member) -> match x.Id with MemberId theId -> theId) - Define.Field("name", SchemaDefinitions.String, "the member's name", fun _ (x : Member) -> x.Name) + Define.Field("id", GuidType, "the member's ID", fun _ (x : Member) -> match x.Id with MemberId theId -> theId) + Define.Field("name", StringType, "the member's name", fun _ (x : Member) -> x.Name) ] ) @@ -146,9 +145,9 @@ module Schema = description = "An organization member", isTypeOf = (fun o -> o :? MeAsAMember), fieldsFn = fun () -> [ - Define.Field("privId", SchemaDefinitions.Guid, "the member's private ID used for authenticating their requests", fun _ (x : MeAsAMember) -> match x.PrivId with MemberPrivateId theId -> theId) - Define.Field("id", SchemaDefinitions.Guid, "the member's ID", fun _ (x : MeAsAMember) -> match x.Id with MemberId theId -> theId) - Define.Field("name", SchemaDefinitions.String, "the member's name", fun _ (x : MeAsAMember) -> x.Name) + Define.Field("privId", GuidType, "the member's private ID used for authenticating their requests", fun _ (x : MeAsAMember) -> match x.PrivId with MemberPrivateId theId -> theId) + Define.Field("id", GuidType, "the member's ID", fun _ (x : MeAsAMember) -> match x.Id with MemberId theId -> theId) + Define.Field("name", StringType, "the member's name", fun _ (x : MeAsAMember) -> x.Name) ] ) @@ -158,8 +157,8 @@ module Schema = description = "A chat member is an organization member participating in a chat room", isTypeOf = (fun o -> o :? ChatMember), fieldsFn = fun () -> [ - Define.Field("id", SchemaDefinitions.Guid, "the member's ID", fun _ (x : ChatMember) -> match x.Id with MemberId theId -> theId) - Define.Field("name", SchemaDefinitions.String, "the member's name", fun _ (x : ChatMember) -> x.Name) + Define.Field("id", GuidType, "the member's ID", fun _ (x : ChatMember) -> match x.Id with MemberId theId -> theId) + Define.Field("name", StringType, "the member's name", fun _ (x : ChatMember) -> x.Name) Define.Field("role", memberRoleInChatEnumDef, "the member's role in the chat", fun _ (x : ChatMember) -> x.Role) ] ) @@ -170,9 +169,9 @@ module Schema = description = "A chat member is an organization member participating in a chat room", isTypeOf = (fun o -> o :? MeAsAChatMember), fieldsFn = fun () -> [ - Define.Field("privId", SchemaDefinitions.Guid, "the member's private ID used for authenticating their requests", fun _ (x : MeAsAChatMember) -> match x.PrivId with MemberPrivateId theId -> theId) - Define.Field("id", SchemaDefinitions.Guid, "the member's ID", fun _ (x : MeAsAChatMember) -> match x.Id with MemberId theId -> theId) - Define.Field("name", SchemaDefinitions.String, "the member's name", fun _ (x : MeAsAChatMember) -> x.Name) + Define.Field("privId", GuidType, "the member's private ID used for authenticating their requests", fun _ (x : MeAsAChatMember) -> match x.PrivId with MemberPrivateId theId -> theId) + Define.Field("id", GuidType, "the member's ID", fun _ (x : MeAsAChatMember) -> match x.Id with MemberId theId -> theId) + Define.Field("name", StringType, "the member's name", fun _ (x : MeAsAChatMember) -> x.Name) Define.Field("role", memberRoleInChatEnumDef, "the member's role in the chat", fun _ (x : MeAsAChatMember) -> x.Role) ] ) @@ -183,8 +182,8 @@ module Schema = description = "A chat room as viewed from the outside", isTypeOf = (fun o -> o :? ChatRoom), fieldsFn = fun () -> [ - Define.Field("id", SchemaDefinitions.Guid, "the chat room's ID", fun _ (x : ChatRoom) -> match x.Id with ChatRoomId theId -> theId) - Define.Field("name", SchemaDefinitions.String, "the chat room's name", fun _ (x : ChatRoom) -> x.Name) + Define.Field("id", GuidType, "the chat room's ID", fun _ (x : ChatRoom) -> match x.Id with ChatRoomId theId -> theId) + Define.Field("name", StringType, "the chat room's name", fun _ (x : ChatRoom) -> x.Name) Define.Field("members", ListOf chatMemberDef, "the members in the chat room", fun _ (x : ChatRoom) -> x.Members) ] ) @@ -195,8 +194,8 @@ module Schema = description = "A chat room as viewed by a chat room member", isTypeOf = (fun o -> o :? ChatRoomForMember), fieldsFn = fun () -> [ - Define.Field("id", SchemaDefinitions.Guid, "the chat room's ID", fun _ (x : ChatRoomForMember) -> match x.Id with ChatRoomId theId -> theId) - Define.Field("name", SchemaDefinitions.String, "the chat room's name", fun _ (x : ChatRoomForMember) -> x.Name) + Define.Field("id", GuidType, "the chat room's ID", fun _ (x : ChatRoomForMember) -> match x.Id with ChatRoomId theId -> theId) + Define.Field("name", StringType, "the chat room's name", fun _ (x : ChatRoomForMember) -> x.Name) Define.Field("meAsAChatMember", meAsAChatMemberDef, "the chat member that queried the details", fun _ (x : ChatRoomForMember) -> x.MeAsAChatMember) Define.Field("otherChatMembers", ListOf chatMemberDef, "the chat members excluding the one who queried the details", fun _ (x : ChatRoomForMember) -> x.OtherChatMembers) ] @@ -208,8 +207,8 @@ module Schema = description = "An organization as seen from the outside", isTypeOf = (fun o -> o :? Organization), fieldsFn = fun () -> [ - Define.Field("id", SchemaDefinitions.Guid, "the organization's ID", fun _ (x : Organization) -> match x.Id with OrganizationId theId -> theId) - Define.Field("name", SchemaDefinitions.String, "the organization's name", fun _ (x : Organization) -> x.Name) + Define.Field("id", GuidType, "the organization's ID", fun _ (x : Organization) -> match x.Id with OrganizationId theId -> theId) + Define.Field("name", StringType, "the organization's name", fun _ (x : Organization) -> x.Name) Define.Field("members", ListOf memberDef, "members of this organization", fun _ (x : Organization) -> x.Members) Define.Field("chatRooms", ListOf chatRoomStatsDef, "chat rooms in this organization", fun _ (x : Organization) -> x.ChatRooms) ] @@ -221,8 +220,8 @@ module Schema = description = "An organization as seen by one of the organization's members", isTypeOf = (fun o -> o :? OrganizationForMember), fieldsFn = fun () -> [ - Define.Field("id", SchemaDefinitions.Guid, "the organization's ID", fun _ (x : OrganizationForMember) -> match x.Id with OrganizationId theId -> theId) - Define.Field("name", SchemaDefinitions.String, "the organization's name", fun _ (x : OrganizationForMember) -> x.Name) + Define.Field("id", GuidType, "the organization's ID", fun _ (x : OrganizationForMember) -> match x.Id with OrganizationId theId -> theId) + Define.Field("name", StringType, "the organization's name", fun _ (x : OrganizationForMember) -> x.Name) Define.Field("meAsAMember", meAsAMemberDef, "the member that queried the details", fun _ (x : OrganizationForMember) -> x.MeAsAMember) Define.Field("otherMembers", ListOf memberDef, "members of this organization", fun _ (x : OrganizationForMember) -> x.OtherMembers) Define.Field("chatRooms", ListOf chatRoomStatsDef, "chat rooms in this organization", fun _ (x : OrganizationForMember) -> x.ChatRooms) @@ -235,11 +234,11 @@ module Schema = description = description, isTypeOf = (fun o -> o :? ChatRoomMessage), fieldsFn = fun () -> [ - Define.Field("id", SchemaDefinitions.Guid, "the message's ID", fun _ (x : ChatRoomMessage) -> match x.Id with MessageId theId -> theId) - Define.Field("chatRoomId", SchemaDefinitions.Guid, "the ID of the chat room the message belongs to", fun _ (x : ChatRoomMessage) -> match x.ChatRoomId with ChatRoomId theId -> theId) - Define.Field("date", SchemaDefinitions.Date, "the time the message was received at the server", fun _ (x : ChatRoomMessage) -> x.Date) - Define.Field("authorId", SchemaDefinitions.Guid, "the member ID of the message's author", fun _ (x : ChatRoomMessage) -> match x.AuthorId with MemberId theId -> theId) - Define.Field("text", SchemaDefinitions.String, "the message's text", fun _ (x : ChatRoomMessage) -> x.Text) + Define.Field("id", GuidType, "the message's ID", fun _ (x : ChatRoomMessage) -> match x.Id with MessageId theId -> theId) + Define.Field("chatRoomId", GuidType, "the ID of the chat room the message belongs to", fun _ (x : ChatRoomMessage) -> match x.ChatRoomId with ChatRoomId theId -> theId) + Define.Field("date", DateTimeOffsetType, "the time the message was received at the server", fun _ (x : ChatRoomMessage) -> DateTimeOffset(x.Date, TimeSpan.Zero)) + Define.Field("authorId", GuidType, "the member ID of the message's author", fun _ (x : ChatRoomMessage) -> match x.AuthorId with MemberId theId -> theId) + Define.Field("text", StringType, "the message's text", fun _ (x : ChatRoomMessage) -> x.Text) ] ) @@ -249,7 +248,7 @@ module Schema = description = description, isTypeOf = (fun o -> o :? unit), fieldsFn = fun () -> [ - Define.Field("doNotUse", SchemaDefinitions.Boolean, "this is just to satify the expected structure of this type", fun _ _ -> true) + Define.Field("doNotUse", BooleanType, "this is just to satify the expected structure of this type", fun _ _ -> true) ] ) @@ -259,7 +258,7 @@ module Schema = description = description, isTypeOf = (fun o -> o :? MessageId), fieldsFn = (fun () -> [ - Define.Field("messageId", SchemaDefinitions.Guid, "this is the message ID", fun _ (x : MessageId) -> match x with MessageId theId -> theId) + Define.Field("messageId", GuidType, "this is the message ID", fun _ (x : MessageId) -> match x with MessageId theId -> theId) ]) ) @@ -269,8 +268,8 @@ module Schema = description = description, isTypeOf = (fun o -> o :? (MemberId * string)), fieldsFn = (fun () -> [ - Define.Field("memberId", SchemaDefinitions.Guid, "this is the member's ID", fun _ (mId : MemberId, _ : string) -> match mId with MemberId theId -> theId) - Define.Field("memberName", SchemaDefinitions.String, "this is the member's name", fun _ (_ : MemberId, name : string) -> name) + Define.Field("memberId", GuidType, "this is the member's ID", fun _ (mId : MemberId, _ : string) -> match mId with MemberId theId -> theId) + Define.Field("memberName", StringType, "this is the member's name", fun _ (_ : MemberId, name : string) -> name) ]) ) @@ -316,8 +315,8 @@ module Schema = description = "Something that happened in the chat room, like a new message sent", isTypeOf = (fun o -> o :? ChatRoomEvent), fieldsFn = (fun () -> [ - Define.Field("chatRoomId", SchemaDefinitions.Guid, "the ID of the chat room in which the event happened", fun _ (x : ChatRoomEvent) -> match x.ChatRoomId with ChatRoomId theId -> theId) - Define.Field("time", SchemaDefinitions.Date, "the time the message was received at the server", fun _ (x : ChatRoomEvent) -> x.Time) + Define.Field("chatRoomId", GuidType, "the ID of the chat room in which the event happened", fun _ (x : ChatRoomEvent) -> match x.ChatRoomId with ChatRoomId theId -> theId) + Define.Field("time", DateTimeOffsetType, "the time the message was received at the server", fun _ (x : ChatRoomEvent) -> DateTimeOffset(x.Time, TimeSpan.Zero)) Define.Field("specificData", chatRoomSpecificEventDef, "the event's specific data", fun _ (x : ChatRoomEvent) -> x.SpecificData) ]) ) @@ -354,8 +353,8 @@ module Schema = "enterOrganization", organizationDetailsDef, "makes a new member enter an organization", - [ Define.Input ("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization") - Define.Input ("member", SchemaDefinitions.String, description = "the new member's name") + [ Define.Input ("organizationId", GuidType, description = "the ID of the organization") + Define.Input ("member", StringType, description = "the new member's name") ], fun ctx root -> let organizationId = OrganizationId (ctx.Arg("organizationId")) @@ -394,7 +393,7 @@ module Schema = |> Option.flatten match maybeResult with | None -> - raise (GraphQLException("couldn't enter organization (maybe the ID is incorrect?)")) + raise (GQLMessageException("couldn't enter organization (maybe the ID is incorrect?)")) | Some res -> res ) @@ -402,9 +401,9 @@ module Schema = "createChatRoom", chatRoomDetailsDef, "creates a new chat room for a user", - [ Define.Input ("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization in which the chat room will be created") - Define.Input ("memberId", SchemaDefinitions.Guid, description = "the member's private ID") - Define.Input ("name", SchemaDefinitions.String, description = "the chat room's name") + [ Define.Input ("organizationId", GuidType, description = "the ID of the organization in which the chat room will be created") + Define.Input ("memberId", GuidType, description = "the member's private ID") + Define.Input ("name", StringType, description = "the chat room's name") ], fun ctx root -> let organizationId = OrganizationId (ctx.Arg("organizationId")) @@ -413,7 +412,7 @@ module Schema = memberPrivId |> authenticateMemberInOrganization organizationId - |> mapR + |> Result.map (fun (organization, theMember) -> let newChatRoomId = ChatRoomId (Guid.NewGuid()) let newChatMember : ChatMember_In_Db = @@ -445,9 +444,9 @@ module Schema = "enterChatRoom", chatRoomDetailsDef, "makes a member enter a chat room", - [ Define.Input ("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization the chat room and member are in") - Define.Input ("chatRoomId", SchemaDefinitions.Guid, description = "the ID of the chat room") - Define.Input ("memberId", SchemaDefinitions.Guid, description = "the member's private ID") + [ Define.Input ("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") + Define.Input ("chatRoomId", GuidType, description = "the ID of the chat room") + Define.Input ("memberId", GuidType, description = "the member's private ID") ], fun ctx root -> let organizationId = OrganizationId (ctx.Arg("organizationId")) @@ -456,13 +455,13 @@ module Schema = memberPrivId |> authenticateMemberInOrganization organizationId - |> bindR + |> Result.bind (fun (organization, theMember) -> chatRoomId |> validateChatRoomExistence organization - |> mapR (fun chatRoom -> (organization, chatRoom, theMember)) + |> Result.map (fun chatRoom -> (organization, chatRoom, theMember)) ) - |> mapR + |> Result.map (fun (_, chatRoom, theMember) -> let newChatMember : ChatMember_In_Db = { ChatRoomId = chatRoom.Id @@ -498,11 +497,11 @@ module Schema = ) Define.Field( "leaveChatRoom", - SchemaDefinitions.Boolean, + BooleanType, "makes a member leave a chat room", - [ Define.Input ("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization the chat room and member are in") - Define.Input ("chatRoomId", SchemaDefinitions.Guid, description = "the ID of the chat room") - Define.Input ("memberId", SchemaDefinitions.Guid, description = "the member's private ID") + [ Define.Input ("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") + Define.Input ("chatRoomId", GuidType, description = "the ID of the chat room") + Define.Input ("memberId", GuidType, description = "the member's private ID") ], fun ctx root -> let organizationId = OrganizationId (ctx.Arg("organizationId")) @@ -511,13 +510,13 @@ module Schema = memberPrivId |> authenticateMemberInOrganization organizationId - |> bindR + |> Result.bind (fun (organization, theMember) -> chatRoomId |> validateChatRoomExistence organization - |> mapR (fun chatRoom -> (organization, chatRoom, theMember)) + |> Result.map (fun chatRoom -> (organization, chatRoom, theMember)) ) - |> mapR + |> Result.map (fun (_, chatRoom, theMember) -> FakePersistence.ChatMembers <- FakePersistence.ChatMembers |> Map.remove (chatRoom.Id, theMember.Id) @@ -532,11 +531,11 @@ module Schema = ) Define.Field( "sendChatMessage", - SchemaDefinitions.Boolean, - [ Define.Input("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization the chat room and member are in") - Define.Input("chatRoomId", SchemaDefinitions.Guid, description = "the chat room's ID") - Define.Input("memberId", SchemaDefinitions.Guid, description = "the member's private ID") - Define.Input("text", SchemaDefinitions.String, description = "the chat message's contents")], + BooleanType, + [ Define.Input("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") + Define.Input("chatRoomId", GuidType, description = "the chat room's ID") + Define.Input("memberId", GuidType, description = "the member's private ID") + Define.Input("text", StringType, description = "the chat message's contents")], fun ctx _ -> let organizationId = OrganizationId (ctx.Arg("organizationId")) let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) @@ -545,13 +544,13 @@ module Schema = memberPrivId |> authenticateMemberInOrganization organizationId - |> bindR + |> Result.bind (fun (organization, theMember) -> chatRoomId |> validateChatRoomExistence organization - |> mapR (fun chatRoom -> (organization, chatRoom, theMember)) + |> Result.map (fun chatRoom -> (organization, chatRoom, theMember)) ) - |> mapR + |> Result.map (fun (_, chatRoom, theMember) -> let newChatRoomMessage = { Id = MessageId (Guid.NewGuid()) @@ -574,12 +573,12 @@ module Schema = ) Define.Field( "editChatMessage", - SchemaDefinitions.Boolean, - [ Define.Input("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization the chat room and member are in") - Define.Input("chatRoomId", SchemaDefinitions.Guid, description = "the chat room's ID") - Define.Input("memberId", SchemaDefinitions.Guid, description = "the member's private ID") - Define.Input("messageId", SchemaDefinitions.Guid, description = "the existing message's ID") - Define.Input("text", SchemaDefinitions.String, description = "the chat message's contents")], + BooleanType, + [ Define.Input("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") + Define.Input("chatRoomId", GuidType, description = "the chat room's ID") + Define.Input("memberId", GuidType, description = "the member's private ID") + Define.Input("messageId", GuidType, description = "the existing message's ID") + Define.Input("text", StringType, description = "the chat message's contents")], fun ctx _ -> let organizationId = OrganizationId (ctx.Arg("organizationId")) let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) @@ -589,14 +588,14 @@ module Schema = memberPrivId |> authenticateMemberInOrganization organizationId - |> bindR + |> Result.bind (fun (organization, theMember) -> chatRoomId |> validateChatRoomExistence organization - |> bindR (fun chatRoom -> messageId |> validateMessageExistence chatRoom |> mapR (fun x -> (chatRoom, x))) - |> mapR (fun (chatRoom, chatMessage) -> (organization, chatRoom, theMember, chatMessage)) + |> Result.bind (fun chatRoom -> messageId |> validateMessageExistence chatRoom |> Result.map (fun x -> (chatRoom, x))) + |> Result.map (fun (chatRoom, chatMessage) -> (organization, chatRoom, theMember, chatMessage)) ) - |> mapR + |> Result.map (fun (_, chatRoom, theMember, chatMessage) -> let newChatRoomMessage = { Id = chatMessage.Id @@ -619,11 +618,11 @@ module Schema = ) Define.Field( "deleteChatMessage", - SchemaDefinitions.Boolean, - [ Define.Input("organizationId", SchemaDefinitions.Guid, description = "the ID of the organization the chat room and member are in") - Define.Input("chatRoomId", SchemaDefinitions.Guid, description = "the chat room's ID") - Define.Input("memberId", SchemaDefinitions.Guid, description = "the member's private ID") - Define.Input("messageId", SchemaDefinitions.Guid, description = "the existing message's ID")], + BooleanType, + [ Define.Input("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") + Define.Input("chatRoomId", GuidType, description = "the chat room's ID") + Define.Input("memberId", GuidType, description = "the member's private ID") + Define.Input("messageId", GuidType, description = "the existing message's ID")], fun ctx _ -> let organizationId = OrganizationId (ctx.Arg("organizationId")) let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) @@ -632,14 +631,14 @@ module Schema = memberPrivId |> authenticateMemberInOrganization organizationId - |> bindR + |> Result.bind (fun (organization, theMember) -> chatRoomId |> validateChatRoomExistence organization - |> bindR (fun chatRoom -> messageId |> validateMessageExistence chatRoom |> mapR (fun x -> (chatRoom, x))) - |> mapR (fun (chatRoom, chatMessage) -> (organization, chatRoom, theMember, chatMessage)) + |> Result.bind (fun chatRoom -> messageId |> validateMessageExistence chatRoom |> Result.map (fun x -> (chatRoom, x))) + |> Result.map (fun (chatRoom, chatMessage) -> (organization, chatRoom, theMember, chatMessage)) ) - |> mapR + |> Result.map (fun (_, chatRoom, theMember, chatMessage) -> FakePersistence.ChatRoomMessages <- FakePersistence.ChatRoomMessages @@ -661,7 +660,7 @@ module Schema = description = "contains general request information", isTypeOf = (fun o -> o :? Root), fieldsFn = fun () -> - [ Define.Field("requestId", SchemaDefinitions.String, "The request's unique ID.", fun _ (r : Root) -> r.RequestId) ] + [ Define.Field("requestId", StringType, "The request's unique ID.", fun _ (r : Root) -> r.RequestId) ] ) let subscription = @@ -673,8 +672,8 @@ module Schema = rootDef, chatRoomEventDef, "events related to a specific chat room", - [ Define.Input("chatRoomId", SchemaDefinitions.Guid, description = "the ID of the chat room to listen to events from") - Define.Input("memberId", SchemaDefinitions.Guid, description = "the member's private ID")], + [ Define.Input("chatRoomId", GuidType, description = "the ID of the chat room to listen to events from") + Define.Input("memberId", GuidType, description = "the member's private ID")], (fun ctx _ (chatRoomEvent : ChatRoomEvent) -> let chatRoomIdOfInterest = ChatRoomId (ctx.Arg("chatRoomId")) let memberId = MemberPrivateId (ctx.Arg("memberId")) From e5891f1cee1847699984c37f213358f3622f462f Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Tue, 5 Mar 2024 22:01:50 +0100 Subject: [PATCH 061/100] Update README.md Co-authored-by: Andrii Chebukin --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf3200393..a30b882cb 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ It's type safe. Things like invalid fields or invalid return types will be check ### ASP.NET / Giraffe / Websocket (for GraphQL subscriptions) usage -→ See the [AspNetCore/README.md](src/FSharp.Data.GraphQL.Server.AspNetCore/README.md) +See the [AspNetCore/README.md](src/FSharp.Data.GraphQL.Server.AspNetCore/README.md) ## Demos From 89601accd8a805d64a96f5bb275a9da904fca0d5 Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Tue, 5 Mar 2024 22:36:35 +0100 Subject: [PATCH 062/100] .Samples.ChatApp: removed superfluous `Label` attr. from ItemGroup Co-authored-by: Andrii Chebukin --- .../chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj index 86c795c85..e44d44b6e 100644 --- a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj +++ b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj @@ -5,7 +5,7 @@ FSharp.Data.GraphQL.Samples.ChatApp - + From 65c7c52f719c35a08d44b8ab48fc975c20efb30d Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Tue, 5 Mar 2024 22:52:46 +0100 Subject: [PATCH 063/100] .Samples.ChatApp: launchSettings.json: commandNames according to template As suggested during code review. Co-authored-by: Andrii Chebukin --- samples/chat-app/server/Properties/launchSettings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/chat-app/server/Properties/launchSettings.json b/samples/chat-app/server/Properties/launchSettings.json index 40fef14da..38cd775cc 100644 --- a/samples/chat-app/server/Properties/launchSettings.json +++ b/samples/chat-app/server/Properties/launchSettings.json @@ -9,7 +9,7 @@ }, "profiles": { "http": { - "commandName": "Project", + "commandName": "http", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "http://localhost:5092", @@ -18,7 +18,7 @@ } }, "https": { - "commandName": "Project", + "commandName": "https", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:7122;http://localhost:5092", From 3af6a1156b47f265edc2d31730f1dcc3e268854b Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Tue, 5 Mar 2024 22:54:30 +0100 Subject: [PATCH 064/100] Samples.StarWarsApi.fsproj : sorting Project References by name As suggested during code review. Co-authored-by: Andrii Chebukin --- .../FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj index 1df66aeba..1e7ee9ed3 100644 --- a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj +++ b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj @@ -28,10 +28,10 @@ + - From 1552bd4b08ef9ac9df761626c81fd5974226d6b0 Mon Sep 17 00:00:00 2001 From: valber Date: Tue, 5 Mar 2024 22:30:11 +0100 Subject: [PATCH 065/100] Using interpolated strings instead of sprintf at many places --- samples/chat-app/server/Exceptions.fs | 12 ++++----- .../Giraffe/HttpHandlers.fs | 11 ++++---- .../GraphQLWebsocketMiddleware.fs | 12 ++++----- .../Serialization/GraphQLQueryDecoding.fs | 14 +++++----- .../Serialization/JsonConverters.fs | 10 +++---- .../AspNetCore/SerializationTests.fs | 26 +++++++++---------- 6 files changed, 43 insertions(+), 42 deletions(-) diff --git a/samples/chat-app/server/Exceptions.fs b/samples/chat-app/server/Exceptions.fs index 9ca8e6c0a..662349875 100644 --- a/samples/chat-app/server/Exceptions.fs +++ b/samples/chat-app/server/Exceptions.fs @@ -3,23 +3,23 @@ module FSharp.Data.GraphQL.Samples.ChatApp.Exceptions open FSharp.Data.GraphQL let Member_With_This_Name_Already_Exists (theName : string) : GraphQLException = - GQLMessageException(sprintf "member with name \"%s\" already exists" theName) + GQLMessageException $"member with name \"%s{theName}\" already exists" let Organization_Doesnt_Exist (theId : OrganizationId) : GraphQLException = match theId with | OrganizationId x -> - GQLMessageException (sprintf "organization with ID \"%s\" doesn't exist" (x.ToString())) + GQLMessageException $"organization with ID \"%s{x.ToString()}\" doesn't exist" let ChatRoom_Doesnt_Exist (theId : ChatRoomId) : GraphQLException = - GQLMessageException(sprintf "chat room with ID \"%s\" doesn't exist" (theId.ToString())) + GQLMessageException $"chat room with ID \"%s{theId.ToString()}\" doesn't exist" let PrivMember_Doesnt_Exist (theId : MemberPrivateId) : GraphQLException = match theId with | MemberPrivateId x -> - GQLMessageException(sprintf "member with private ID \"%s\" doesn't exist" (x.ToString())) + GQLMessageException $"member with private ID \"%s{x.ToString()}\" doesn't exist" let Member_Isnt_Part_Of_Org () : GraphQLException = - GQLMessageException("this member is not part of this organization") + GQLMessageException "this member is not part of this organization" let ChatRoom_Isnt_Part_Of_Org () : GraphQLException = - GQLMessageException("this chat room is not part of this organization") \ No newline at end of file + GQLMessageException "this chat room is not part of this organization" \ No newline at end of file diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 1a6d5639c..307264950 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -76,7 +76,8 @@ module HttpHandlers = return Ok deserialized with | :? GraphQLException as ex -> - return Result.Error [sprintf "%s" (ex.Message)] + logger.LogError(``exception`` = ex, message = "Error while deserializing request.") + return Result.Error [$"%s{ex.Message}\n%s{ex.ToString()}"] } let applyPlanExecutionResult (result : GQLExecutionResult) = @@ -106,7 +107,7 @@ module HttpHandlers = | None -> let! result = executor.AsyncExecute (IntrospectionQuery.Definition) |> Async.StartAsTask if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) + logger.LogDebug($"Result metadata: %A{result.Metadata}") else () return! result |> applyPlanExecutionResult @@ -124,7 +125,7 @@ module HttpHandlers = httpOk cancellationToken interceptor serializerOptions (GQLResponse.RequestError (docId, probDetails)) next ctx | Ok query -> if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(sprintf "Received query: %A" query) + logger.LogDebug($"Received query: %A{query}") else () let root = rootFactory(ctx) @@ -139,7 +140,7 @@ module HttpHandlers = variables = variables )|> Async.StartAsTask if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) + logger.LogDebug($"Result metadata: %A{result.Metadata}") else () return! result |> applyPlanExecutionResult @@ -147,7 +148,7 @@ module HttpHandlers = if ctx.Request.Headers.ContentLength.GetValueOrDefault(0) = 0 then let! result = executor.AsyncExecute (IntrospectionQuery.Definition) |> Async.StartAsTask if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(sprintf "Result metadata: %A" result.Metadata) + logger.LogDebug($"Result metadata: %A{result.Metadata}") else () return! result |> applyPlanExecutionResult diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index bb2879f60..084cd35c1 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -194,7 +194,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti output |> sendOutput id | SubscriptionErrors (output, errors) -> - printfn "Subscription errors: %s" (String.Join('\n', errors |> Seq.map (fun x -> sprintf "- %s" x.Message))) + printfn "Subscription errors: %s" (String.Join('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) Task.FromResult(()) let sendDeferredResponseOutput id deferredResult = @@ -204,7 +204,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti output |> sendOutput id | DeferredErrors (obj, errors, _) -> - printfn "Deferred response errors: %s" (String.Join('\n', errors |> Seq.map (fun x -> sprintf "- %s" x.Message))) + printfn "Deferred response errors: %s" (String.Join('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) Task.FromResult(()) let sendDeferredResultDelayedBy (cancToken: CancellationToken) (ms: int) id deferredResult = @@ -233,12 +233,12 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti do! data |> sendOutput id | RequestError problemDetails -> - printfn "Request error: %s" (String.Join('\n', problemDetails |> Seq.map (fun x -> sprintf "- %s" x.Message))) + printfn "Request error: %s" (String.Join('\n', problemDetails |> Seq.map (fun x -> $"- %s{x.Message}"))) } let getStrAddendumOfOptionalPayload optionalPayload = optionalPayload - |> Option.map (fun payloadStr -> sprintf " with payload: %A" payloadStr) + |> Option.map (fun payloadStr -> $" with payload: %A{payloadStr}") |> Option.defaultWith (fun () -> "") let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) = @@ -291,7 +291,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then do! socket.CloseAsync( enum CustomWebSocketStatus.subscriberAlreadyExists, - sprintf "Subscriber for %s already exists" id, + $"Subscriber for %s{id} already exists", CancellationToken.None) else let variables = ImmutableDictionary.CreateRange(query.Variables |> Map.map (fun _ value -> value :?> JsonElement)) @@ -375,7 +375,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti |> waitForConnectionInitAndRespondToClient options.SerializerOptions options.WebsocketOptions.ConnectionInitTimeoutInMs match connectionInitResult with | Result.Error errMsg -> - logger.LogWarning("{warningmsg}", (sprintf "%A" errMsg)) + logger.LogWarning("{warningmsg}", ($"%A{errMsg}")) | Ok _ -> let longRunningCancellationToken = (CancellationTokenSource.CreateLinkedTokenSource(ctx.RequestAborted, applicationLifetime.ApplicationStopping).Token) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs index 9f64cba2a..a2aa221a4 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs @@ -8,7 +8,7 @@ module GraphQLQueryDecoding = open FsToolkit.ErrorHandling let genericErrorContentForDoc (docId: int) (message: string) = - struct (docId, [GQLProblemDetails.Create ((sprintf "%s" message), Skip)]) + struct (docId, [GQLProblemDetails.Create (message, Skip)]) let genericFinalErrorForDoc (docId: int) (message: string) = Result.Error (genericErrorContentForDoc docId message) @@ -22,7 +22,7 @@ module GraphQLQueryDecoding = try if (not (variableValuesObj.RootElement.ValueKind.Equals(JsonValueKind.Object))) then let offendingValueKind = variableValuesObj.RootElement.ValueKind - return! Result.Error (sprintf "\"variables\" must be an object, but here it is \"%A\" instead" offendingValueKind) + return! Result.Error ($"\"variables\" must be an object, but here it is \"%A{offendingValueKind}\" instead") else let providedVariableValues = variableValuesObj.RootElement.EnumerateObject() |> List.ofSeq return! Ok @@ -46,9 +46,9 @@ module GraphQLQueryDecoding = |> Map.ofList) with | :? JsonException as ex -> - return! Result.Error (sprintf "%s" (ex.Message)) + return! Result.Error (ex.Message) | :? GraphQLException as ex -> - return! Result.Error (sprintf "%s" (ex.Message)) + return! Result.Error (ex.Message) | ex -> printfn "%s" (ex.ToString()) return! Result.Error ("Something unexpected happened during the parsing of this request.") @@ -67,9 +67,9 @@ module GraphQLQueryDecoding = return! executor.CreateExecutionPlan(query) with | :? JsonException as ex -> - return! genericFinalError (sprintf "%s" (ex.Message)) + return! genericFinalError (ex.Message) | :? GraphQLException as ex -> - return! genericFinalError (sprintf "%s" (ex.Message)) + return! genericFinalError (ex.Message) } executionPlanResult @@ -81,7 +81,7 @@ module GraphQLQueryDecoding = variableValuesObj |> resolveVariables serializerOptions executionPlan.Variables |> Result.map (fun variableValsObj -> (executionPlan, variableValsObj)) - |> Result.mapError (fun x -> sprintf "%s" x |> genericErrorContentForDoc executionPlan.DocumentId) + |> Result.mapError (genericErrorContentForDoc executionPlan.DocumentId) ) |> Result.map (fun (executionPlan, variables) -> { ExecutionPlan = executionPlan diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index 4a876e14e..d58c975f7 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -37,7 +37,7 @@ type ClientMessageConverter<'Root>(executor : Executor<'Root>) = if reader.Read() then getOptionalString(&reader) else - raiseInvalidMsg <| sprintf "was expecting a value for property \"%s\"" propertyName + raiseInvalidMsg <| $"was expecting a value for property \"%s{propertyName}\"" let requireId (raw : RawMessage) : Result = match raw.Id with @@ -56,7 +56,7 @@ type ClientMessageConverter<'Root>(executor : Executor<'Root>) = | Some subscribePayload -> match subscribePayload.Query with | None -> - invalidMsg <| sprintf "there was no query in the client's subscribe message!" + invalidMsg <| "there was no query in the client's subscribe message!" | Some query -> query |> GraphQLQueryDecoding.decodeGraphQLQuery serializerOptions executor subscribePayload.OperationName subscribePayload.Variables @@ -65,7 +65,7 @@ type ClientMessageConverter<'Root>(executor : Executor<'Root>) = let readRawMessage (reader : byref, options: JsonSerializerOptions) : RawMessage = if not (reader.TokenType.Equals(JsonTokenType.StartObject)) - then raise (new JsonException((sprintf "reader's first token was not \"%A\", but \"%A\"" JsonTokenType.StartObject reader.TokenType))) + then raise (new JsonException($"reader's first token was not \"%A{JsonTokenType.StartObject}\", but \"%A{reader.TokenType}\"")) else let mutable id : string option = None let mutable theType : string option = None @@ -79,7 +79,7 @@ type ClientMessageConverter<'Root>(executor : Executor<'Root>) = | "payload" -> payload <- Some <| JsonDocument.ParseValue(&reader) | other -> - raiseInvalidMsg <| sprintf "unknown property \"%s\"" other + raiseInvalidMsg <| $"unknown property \"%s{other}\"" match theType with | None -> @@ -115,7 +115,7 @@ type ClientMessageConverter<'Root>(executor : Executor<'Root>) = |> Result.map Subscribe |> unpackRopResult | other -> - raiseInvalidMsg <| sprintf "invalid type \"%s\" specified by client." other + raiseInvalidMsg <| $"invalid type \"%s{other}\" specified by client." diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs index eabd0b582..a1e99b7fe 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs @@ -22,7 +22,7 @@ let ``Deserializes ConnectionInit correctly`` () = match result with | ConnectionInit None -> () // <-- expected | other -> - Assert.Fail(sprintf "unexpected actual value: '%A'" other) + Assert.Fail($"unexpected actual value: '%A{other}'") [] let ``Deserializes ConnectionInit with payload correctly`` () = @@ -35,7 +35,7 @@ let ``Deserializes ConnectionInit with payload correctly`` () = match result with | ConnectionInit _ -> () // <-- expected | other -> - Assert.Fail(sprintf "unexpected actual value: '%A'" other) + Assert.Fail($"unexpected actual value: '%A{other}'") [] let ``Deserializes ClientPing correctly`` () = @@ -48,7 +48,7 @@ let ``Deserializes ClientPing correctly`` () = match result with | ClientPing None -> () // <-- expected | other -> - Assert.Fail(sprintf "unexpected actual value '%A'" other) + Assert.Fail($"unexpected actual value '%A{other}'") [] let ``Deserializes ClientPing with payload correctly`` () = @@ -61,7 +61,7 @@ let ``Deserializes ClientPing with payload correctly`` () = match result with | ClientPing _ -> () // <-- expected | other -> - Assert.Fail(sprintf "unexpected actual value '%A" other) + Assert.Fail($"unexpected actual value '%A{other}'") [] let ``Deserializes ClientPong correctly`` () = @@ -74,7 +74,7 @@ let ``Deserializes ClientPong correctly`` () = match result with | ClientPong None -> () // <-- expected | other -> - Assert.Fail(sprintf "unexpected actual value: '%A'" other) + Assert.Fail($"unexpected actual value: '%A{other}'") [] let ``Deserializes ClientPong with payload correctly`` () = @@ -87,7 +87,7 @@ let ``Deserializes ClientPong with payload correctly`` () = match result with | ClientPong _ -> () // <-- expected | other -> - Assert.Fail(sprintf "unexpected actual value: '%A'" other) + Assert.Fail($"unexpected actual value: '%A{other}'") [] let ``Deserializes ClientComplete correctly``() = @@ -101,7 +101,7 @@ let ``Deserializes ClientComplete correctly``() = | ClientComplete id -> Assert.Equal("65fca2b5-f149-4a70-a055-5123dea4628f", id) | other -> - Assert.Fail(sprintf "unexpected actual value: '%A'" other) + Assert.Fail($"unexpected actual value: '%A{other}'") [] let ``Deserializes client subscription correctly`` () = @@ -134,24 +134,24 @@ let ``Deserializes client subscription correctly`` () = | StringValue theValue -> Assert.Equal("1", theValue) | other -> - Assert.Fail(sprintf "expected arg to be a StringValue, but it was: %A" other) + Assert.Fail($"expected arg to be a StringValue, but it was: %A{other}") Assert.Equal(3, watchMoonField.SelectionSet.Length) match watchMoonField.SelectionSet.[0] with | Field firstField -> Assert.Equal("id", firstField.Name) | other -> - Assert.Fail(sprintf "expected field to be a Field, but it was: %A" other) + Assert.Fail($"expected field to be a Field, but it was: %A{other}") match watchMoonField.SelectionSet.[1] with | Field secondField -> Assert.Equal("name", secondField.Name) | other -> - Assert.Fail(sprintf "expected field to be a Field, but it was: %A" other) + Assert.Fail($"expected field to be a Field, but it was: %A{other}") match watchMoonField.SelectionSet.[2] with | Field thirdField -> Assert.Equal("isMoon", thirdField.Name) | other -> - Assert.Fail(sprintf "expected field to be a Field, but it was: %A" other) + Assert.Fail($"expected field to be a Field, but it was: %A{other}") | somethingElse -> - Assert.Fail(sprintf "expected it to be a field, but it was: %A" somethingElse) + Assert.Fail($"expected it to be a field, but it was: %A{somethingElse}") | other -> - Assert.Fail(sprintf "unexpected actual value: '%A" other) \ No newline at end of file + Assert.Fail($"unexpected actual value: '%A{other}'") \ No newline at end of file From 5c5171459fb57748a11e8fb28433d68fc0e21c76 Mon Sep 17 00:00:00 2001 From: valber Date: Tue, 5 Mar 2024 22:49:39 +0100 Subject: [PATCH 066/100] .Samples.ChatApp: better name for logger According to suggestion during code review --- samples/chat-app/server/Program.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/chat-app/server/Program.fs b/samples/chat-app/server/Program.fs index cfdb5a8a5..1388cfd56 100644 --- a/samples/chat-app/server/Program.fs +++ b/samples/chat-app/server/Program.fs @@ -45,7 +45,7 @@ module Program = .UseGiraffe (HttpHandlers.handleGraphQL applicationLifetime.ApplicationStopping - (loggerFactory.CreateLogger("HttpHandlers.handlerGraphQL")) + (loggerFactory.CreateLogger("FSharp.Data.GraphQL.Server.AspNetCore.HttpHandlers.handleGraphQL")) ) app.Run() From 51851eaa3622dbbed9acced793b1b8b083ea1de8 Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 6 Mar 2024 20:46:49 +0100 Subject: [PATCH 067/100] .Server.AspNetCore: formatted code with Fantomas according to request in pull request --- samples/chat-app/server/DomainModel.fs | 101 +- samples/chat-app/server/Exceptions.fs | 20 +- samples/chat-app/server/FakePersistence.fs | 55 +- samples/chat-app/server/Program.fs | 32 +- samples/chat-app/server/Schema.fs | 1511 +++++++++-------- .../Exceptions.fs | 2 +- .../Giraffe/HttpHandlers.fs | 239 ++- .../GraphQLOptions.fs | 24 +- .../GraphQLSubscriptionsManagement.fs | 40 +- .../GraphQLWebsocketMiddleware.fs | 641 ++++--- .../Messages.fs | 40 +- .../Serialization/GraphQLQueryDecoding.fs | 154 +- .../Serialization/JsonConverters.fs | 314 ++-- .../StartupExtensions.fs | 62 +- .../AspNetCore/InvalidMessageTests.fs | 42 +- .../AspNetCore/SerializationTests.fs | 108 +- .../AspNetCore/TestSchema.fs | 291 ++-- 17 files changed, 1899 insertions(+), 1777 deletions(-) diff --git a/samples/chat-app/server/DomainModel.fs b/samples/chat-app/server/DomainModel.fs index 6a02027d5..5d88190a6 100644 --- a/samples/chat-app/server/DomainModel.fs +++ b/samples/chat-app/server/DomainModel.fs @@ -12,95 +12,82 @@ type ChatRoomId = ChatRoomId of Guid type MessageId = MessageId of Guid type MemberRoleInChat = - | ChatAdmin - | ChatGuest + | ChatAdmin + | ChatGuest -type ChatRoomMessage = - { Id : MessageId +type ChatRoomMessage = { + Id : MessageId ChatRoomId : ChatRoomId Date : DateTime AuthorId : MemberId - Text : string } + Text : string +} type ChatRoomSpecificEvent = - | NewMessage of ChatRoomMessage - | EditedMessage of ChatRoomMessage - | DeletedMessage of MessageId - | MemberJoined of MemberId * string - | MemberLeft of MemberId * string - -type ChatRoomEvent = - { ChatRoomId : ChatRoomId + | NewMessage of ChatRoomMessage + | EditedMessage of ChatRoomMessage + | DeletedMessage of MessageId + | MemberJoined of MemberId * string + | MemberLeft of MemberId * string + +type ChatRoomEvent = { + ChatRoomId : ChatRoomId Time : DateTime - SpecificData : ChatRoomSpecificEvent } + SpecificData : ChatRoomSpecificEvent +} // // Persistence model // -type Member_In_Db = - { PrivId : MemberPrivateId - Id : MemberId - Name : string } +type Member_In_Db = { PrivId : MemberPrivateId; Id : MemberId; Name : string } -type ChatMember_In_Db = - { ChatRoomId : ChatRoomId - MemberId : MemberId - Role : MemberRoleInChat } +type ChatMember_In_Db = { ChatRoomId : ChatRoomId; MemberId : MemberId; Role : MemberRoleInChat } -type ChatRoom_In_Db = - { Id : ChatRoomId - Name : string - Members : MemberId list } +type ChatRoom_In_Db = { Id : ChatRoomId; Name : string; Members : MemberId list } -type Organization_In_Db = - { Id : OrganizationId +type Organization_In_Db = { + Id : OrganizationId Name : string - Members : MemberId list - ChatRooms : ChatRoomId list } + Members : MemberId list + ChatRooms : ChatRoomId list +} // // GraphQL models // -type Member = - { Id : MemberId - Name : string } +type Member = { Id : MemberId; Name : string } -type MeAsAMember = - { PrivId : MemberPrivateId - Id : MemberId - Name : string } +type MeAsAMember = { PrivId : MemberPrivateId; Id : MemberId; Name : string } -type ChatMember = - { Id : MemberId - Name : string - Role : MemberRoleInChat } +type ChatMember = { Id : MemberId; Name : string; Role : MemberRoleInChat } -type MeAsAChatMember = - { PrivId : MemberPrivateId +type MeAsAChatMember = { + PrivId : MemberPrivateId Id : MemberId Name : string - Role : MemberRoleInChat } + Role : MemberRoleInChat +} -type ChatRoom = - { Id : ChatRoomId - Name : string - Members: ChatMember list } +type ChatRoom = { Id : ChatRoomId; Name : string; Members : ChatMember list } -type ChatRoomForMember = - { Id : ChatRoomId +type ChatRoomForMember = { + Id : ChatRoomId Name : string MeAsAChatMember : MeAsAChatMember - OtherChatMembers: ChatMember list } + OtherChatMembers : ChatMember list +} -type Organization = - { Id : OrganizationId +type Organization = { + Id : OrganizationId Name : string Members : Member list - ChatRooms : ChatRoom list } + ChatRooms : ChatRoom list +} -type OrganizationForMember = - { Id : OrganizationId +type OrganizationForMember = { + Id : OrganizationId Name : string MeAsAMember : MeAsAMember OtherMembers : Member list - ChatRooms : ChatRoom list } + ChatRooms : ChatRoom list +} diff --git a/samples/chat-app/server/Exceptions.fs b/samples/chat-app/server/Exceptions.fs index 662349875..674aac1fb 100644 --- a/samples/chat-app/server/Exceptions.fs +++ b/samples/chat-app/server/Exceptions.fs @@ -3,23 +3,19 @@ module FSharp.Data.GraphQL.Samples.ChatApp.Exceptions open FSharp.Data.GraphQL let Member_With_This_Name_Already_Exists (theName : string) : GraphQLException = - GQLMessageException $"member with name \"%s{theName}\" already exists" + GQLMessageException $"member with name \"%s{theName}\" already exists" let Organization_Doesnt_Exist (theId : OrganizationId) : GraphQLException = - match theId with - | OrganizationId x -> - GQLMessageException $"organization with ID \"%s{x.ToString()}\" doesn't exist" + match theId with + | OrganizationId x -> GQLMessageException $"organization with ID \"%s{x.ToString ()}\" doesn't exist" let ChatRoom_Doesnt_Exist (theId : ChatRoomId) : GraphQLException = - GQLMessageException $"chat room with ID \"%s{theId.ToString()}\" doesn't exist" + GQLMessageException $"chat room with ID \"%s{theId.ToString ()}\" doesn't exist" let PrivMember_Doesnt_Exist (theId : MemberPrivateId) : GraphQLException = - match theId with - | MemberPrivateId x -> - GQLMessageException $"member with private ID \"%s{x.ToString()}\" doesn't exist" + match theId with + | MemberPrivateId x -> GQLMessageException $"member with private ID \"%s{x.ToString ()}\" doesn't exist" -let Member_Isnt_Part_Of_Org () : GraphQLException = - GQLMessageException "this member is not part of this organization" +let Member_Isnt_Part_Of_Org () : GraphQLException = GQLMessageException "this member is not part of this organization" -let ChatRoom_Isnt_Part_Of_Org () : GraphQLException = - GQLMessageException "this chat room is not part of this organization" \ No newline at end of file +let ChatRoom_Isnt_Part_Of_Org () : GraphQLException = GQLMessageException "this chat room is not part of this organization" diff --git a/samples/chat-app/server/FakePersistence.fs b/samples/chat-app/server/FakePersistence.fs index 975761236..a6024df7d 100644 --- a/samples/chat-app/server/FakePersistence.fs +++ b/samples/chat-app/server/FakePersistence.fs @@ -2,38 +2,33 @@ namespace FSharp.Data.GraphQL.Samples.ChatApp open System -type FakePersistence() = - static let mutable _members = Map.empty - static let mutable _chatMembers = Map.empty - static let mutable _chatRoomMessages = Map.empty - static let mutable _chatRooms = Map.empty - static let mutable _organizations = - let newId = OrganizationId (Guid.Parse("51f823ef-2294-41dc-9f39-a4b9a237317a")) - ( newId, - { Organization_In_Db.Id = newId - Name = "Public" - Members = [] - ChatRooms = [] } - ) - |> List.singleton - |> Map.ofList +type FakePersistence () = + static let mutable _members = Map.empty + static let mutable _chatMembers = Map.empty + static let mutable _chatRoomMessages = Map.empty + static let mutable _chatRooms = Map.empty + static let mutable _organizations = + let newId = OrganizationId (Guid.Parse ("51f823ef-2294-41dc-9f39-a4b9a237317a")) + (newId, { Organization_In_Db.Id = newId; Name = "Public"; Members = []; ChatRooms = [] }) + |> List.singleton + |> Map.ofList - static member Members - with get() = _members - and set(v) = _members <- v + static member Members + with get () = _members + and set (v) = _members <- v - static member ChatMembers - with get() = _chatMembers - and set(v) = _chatMembers <- v + static member ChatMembers + with get () = _chatMembers + and set (v) = _chatMembers <- v - static member ChatRoomMessages - with get() = _chatRoomMessages - and set(v) = _chatRoomMessages <- v + static member ChatRoomMessages + with get () = _chatRoomMessages + and set (v) = _chatRoomMessages <- v - static member ChatRooms - with get() = _chatRooms - and set(v) = _chatRooms <- v + static member ChatRooms + with get () = _chatRooms + and set (v) = _chatRooms <- v - static member Organizations - with get() = _organizations - and set(v) = _organizations <- v \ No newline at end of file + static member Organizations + with get () = _organizations + and set (v) = _organizations <- v diff --git a/samples/chat-app/server/Program.fs b/samples/chat-app/server/Program.fs index 1388cfd56..786865406 100644 --- a/samples/chat-app/server/Program.fs +++ b/samples/chat-app/server/Program.fs @@ -13,42 +13,36 @@ open Microsoft.Extensions.Logging module Program = - let rootFactory (ctx : HttpContext) : Root = - { RequestId = ctx.TraceIdentifier } + let rootFactory (ctx : HttpContext) : Root = { RequestId = ctx.TraceIdentifier } let errorHandler (ex : Exception) (log : ILogger) = - log.LogError(EventId(), ex, "An unhandled exception has occurred while executing this request.") + log.LogError (EventId (), ex, "An unhandled exception has occurred while executing this request.") clearResponse >=> setStatusCode 500 [] let main args = - let builder = WebApplication.CreateBuilder(args) + let builder = WebApplication.CreateBuilder (args) builder.Services .AddGiraffe() - .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) - .AddGraphQLOptions( - Schema.executor, - rootFactory, - "/ws" - ) + .Configure(Action (fun x -> x.AllowSynchronousIO <- true)) + .AddGraphQLOptions (Schema.executor, rootFactory, "/ws") |> ignore - let app = builder.Build() + let app = builder.Build () - let applicationLifetime = app.Services.GetRequiredService() - let loggerFactory = app.Services.GetRequiredService() + let applicationLifetime = app.Services.GetRequiredService () + let loggerFactory = app.Services.GetRequiredService () app .UseGiraffeErrorHandler(errorHandler) .UseWebSockets() .UseWebSocketsForGraphQL() - .UseGiraffe - (HttpHandlers.handleGraphQL + .UseGiraffe ( + HttpHandlers.handleGraphQL applicationLifetime.ApplicationStopping - (loggerFactory.CreateLogger("FSharp.Data.GraphQL.Server.AspNetCore.HttpHandlers.handleGraphQL")) - ) + (loggerFactory.CreateLogger ("FSharp.Data.GraphQL.Server.AspNetCore.HttpHandlers.handleGraphQL")) + ) - app.Run() + app.Run () 0 // Exit code - diff --git a/samples/chat-app/server/Schema.fs b/samples/chat-app/server/Schema.fs index cbc7654d1..3786c9e81 100644 --- a/samples/chat-app/server/Schema.fs +++ b/samples/chat-app/server/Schema.fs @@ -5,696 +5,871 @@ open FSharp.Data.GraphQL.Types open FsToolkit.ErrorHandling open System -type Root = - { RequestId : string } +type Root = { RequestId : string } module MapFrom = - let memberInDb_To_Member (x : Member_In_Db) : Member = - { Id = x.Id - Name = x.Name } - - let memberInDb_To_MeAsAMember (x : Member_In_Db) : MeAsAMember = - { PrivId = x.PrivId - Id = x.Id - Name = x.Name } - - let chatMemberInDb_To_ChatMember (membersToGetDetailsFrom : Member_In_Db seq) (x : ChatMember_In_Db) : ChatMember = - let memberDetails = membersToGetDetailsFrom |> Seq.find (fun m -> m.Id = x.MemberId) - { Id = x.MemberId - Name = memberDetails.Name - Role = x.Role } - - let chatMemberInDb_To_MeAsAChatMember (membersToGetDetailsFrom : Member_In_Db seq) (x : ChatMember_In_Db) : MeAsAChatMember = - let memberDetails = membersToGetDetailsFrom |> Seq.find (fun m -> m.Id = x.MemberId) - { PrivId = memberDetails.PrivId - Id = x.MemberId - Name = memberDetails.Name - Role = x.Role } - - let chatRoomInDb_To_ChatRoom (membersToGetDetailsFrom : Member_In_Db seq) (x : ChatRoom_In_Db) : ChatRoom = - { Id = x.Id - Name = x.Name - Members = - FakePersistence.ChatMembers.Values - |> Seq.filter (fun m -> x.Members |> List.contains m.MemberId) - |> Seq.map (chatMemberInDb_To_ChatMember membersToGetDetailsFrom) - |> List.ofSeq } - - let chatRoomInDb_To_ChatRoomForMember (membersToGetDetailsFrom : Member_In_Db seq) (chatMember : ChatMember_In_Db) (x : ChatRoom_In_Db) : ChatRoomForMember = - { Id = x.Id - Name = x.Name - MeAsAChatMember = chatMember |> chatMemberInDb_To_MeAsAChatMember membersToGetDetailsFrom - OtherChatMembers = - FakePersistence.ChatMembers.Values - |> Seq.filter (fun m -> m.MemberId <> chatMember.MemberId && x.Members |> List.contains m.MemberId) - |> Seq.map (chatMemberInDb_To_ChatMember membersToGetDetailsFrom) - |> List.ofSeq } - - let organizationInDb_To_Organization (x : Organization_In_Db) : Organization = - let members = - FakePersistence.Members.Values - |> Seq.filter (fun m -> x.Members |> List.contains m.Id) - { Id = x.Id - Name = x.Name - Members = members |> Seq.map memberInDb_To_Member |> List.ofSeq - ChatRooms = - FakePersistence.ChatRooms.Values - |> Seq.filter (fun c -> x.ChatRooms |> List.contains c.Id) - |> Seq.map (chatRoomInDb_To_ChatRoom members) - |> List.ofSeq } - - let organizationInDb_To_OrganizationForMember (memberId : MemberId) (x : Organization_In_Db) : OrganizationForMember option = - let mapToOrganizationForMemberForMember (memberInDb : Member_In_Db) = - let organizationStats = x |> organizationInDb_To_Organization - { OrganizationForMember.Id = x.Id + let memberInDb_To_Member (x : Member_In_Db) : Member = { Id = x.Id; Name = x.Name } + + let memberInDb_To_MeAsAMember (x : Member_In_Db) : MeAsAMember = { PrivId = x.PrivId; Id = x.Id; Name = x.Name } + + let chatMemberInDb_To_ChatMember (membersToGetDetailsFrom : Member_In_Db seq) (x : ChatMember_In_Db) : ChatMember = + let memberDetails = + membersToGetDetailsFrom + |> Seq.find (fun m -> m.Id = x.MemberId) + { Id = x.MemberId; Name = memberDetails.Name; Role = x.Role } + + let chatMemberInDb_To_MeAsAChatMember (membersToGetDetailsFrom : Member_In_Db seq) (x : ChatMember_In_Db) : MeAsAChatMember = + let memberDetails = + membersToGetDetailsFrom + |> Seq.find (fun m -> m.Id = x.MemberId) + { + PrivId = memberDetails.PrivId + Id = x.MemberId + Name = memberDetails.Name + Role = x.Role + } + + let chatRoomInDb_To_ChatRoom (membersToGetDetailsFrom : Member_In_Db seq) (x : ChatRoom_In_Db) : ChatRoom = { + Id = x.Id Name = x.Name - MeAsAMember = memberInDb |> memberInDb_To_MeAsAMember - OtherMembers = organizationStats.Members |> List.filter(fun m -> m.Id <> memberInDb.Id) - ChatRooms = organizationStats.ChatRooms } - FakePersistence.Members.Values - |> Seq.tryFind (fun m -> m.Id = memberId) - |> Option.map mapToOrganizationForMemberForMember + Members = + FakePersistence.ChatMembers.Values + |> Seq.filter (fun m -> x.Members |> List.contains m.MemberId) + |> Seq.map (chatMemberInDb_To_ChatMember membersToGetDetailsFrom) + |> List.ofSeq + } + + let chatRoomInDb_To_ChatRoomForMember + (membersToGetDetailsFrom : Member_In_Db seq) + (chatMember : ChatMember_In_Db) + (x : ChatRoom_In_Db) + : ChatRoomForMember = + { + Id = x.Id + Name = x.Name + MeAsAChatMember = + chatMember + |> chatMemberInDb_To_MeAsAChatMember membersToGetDetailsFrom + OtherChatMembers = + FakePersistence.ChatMembers.Values + |> Seq.filter (fun m -> + m.MemberId <> chatMember.MemberId + && x.Members |> List.contains m.MemberId) + |> Seq.map (chatMemberInDb_To_ChatMember membersToGetDetailsFrom) + |> List.ofSeq + } + + let organizationInDb_To_Organization (x : Organization_In_Db) : Organization = + let members = + FakePersistence.Members.Values + |> Seq.filter (fun m -> x.Members |> List.contains m.Id) + { + Id = x.Id + Name = x.Name + Members = members |> Seq.map memberInDb_To_Member |> List.ofSeq + ChatRooms = + FakePersistence.ChatRooms.Values + |> Seq.filter (fun c -> x.ChatRooms |> List.contains c.Id) + |> Seq.map (chatRoomInDb_To_ChatRoom members) + |> List.ofSeq + } + + let organizationInDb_To_OrganizationForMember (memberId : MemberId) (x : Organization_In_Db) : OrganizationForMember option = + let mapToOrganizationForMemberForMember (memberInDb : Member_In_Db) = + let organizationStats = x |> organizationInDb_To_Organization + { + OrganizationForMember.Id = x.Id + Name = x.Name + MeAsAMember = memberInDb |> memberInDb_To_MeAsAMember + OtherMembers = + organizationStats.Members + |> List.filter (fun m -> m.Id <> memberInDb.Id) + ChatRooms = organizationStats.ChatRooms + } + FakePersistence.Members.Values + |> Seq.tryFind (fun m -> m.Id = memberId) + |> Option.map mapToOrganizationForMemberForMember module Schema = - let authenticateMemberInOrganization (organizationId : OrganizationId) (memberPrivId : MemberPrivateId) : Result<(Organization_In_Db * Member_In_Db), GraphQLException> = - let maybeOrganization = FakePersistence.Organizations |> Map.tryFind organizationId - let maybeMember = FakePersistence.Members.Values |> Seq.tryFind (fun x -> x.PrivId = memberPrivId) - - match (maybeOrganization, maybeMember) with - | None, _ -> - Error (organizationId |> Exceptions.Organization_Doesnt_Exist) - | _, None -> - Error (memberPrivId |> Exceptions.PrivMember_Doesnt_Exist) - | Some organization, Some theMember -> - if not (organization.Members |> List.contains theMember.Id) then - Error (Exceptions.Member_Isnt_Part_Of_Org()) - else - Ok (organization, theMember) - - let validateChatRoomExistence (organization : Organization_In_Db) (chatRoomId : ChatRoomId) : Result = - match FakePersistence.ChatRooms |> Map.tryFind chatRoomId with - | None -> - Error <| Exceptions.ChatRoom_Doesnt_Exist chatRoomId - | Some chatRoom -> - if not (organization.ChatRooms |> List.contains chatRoom.Id) then - Error <| Exceptions.ChatRoom_Isnt_Part_Of_Org() - else - Ok chatRoom - - let validateMessageExistence (chatRoom : ChatRoom_In_Db) (messageId : MessageId) : Result = - match FakePersistence.ChatRoomMessages |> Map.tryFind (chatRoom.Id, messageId) with - | None -> - Error (GQLMessageException("chat message doesn't exist (anymore)")) - | Some chatMessage -> - Ok chatMessage - - let succeedOrRaiseGraphQLEx<'T> (result : Result<'T, GraphQLException>) : 'T = - match result with - | Error ex -> - raise ex - | Ok s -> - s - - let chatRoomEvents_subscription_name = "chatRoomEvents" - - let memberRoleInChatEnumDef = - Define.Enum( - name = nameof MemberRoleInChat, - options = [ - Define.EnumValue(ChatAdmin.ToString(), ChatAdmin) - Define.EnumValue(ChatGuest.ToString(), ChatGuest) - ] - ) - - let memberDef = - Define.Object( - name = nameof Member, - description = "An organization member", - isTypeOf = (fun o -> o :? Member), - fieldsFn = fun () -> [ - Define.Field("id", GuidType, "the member's ID", fun _ (x : Member) -> match x.Id with MemberId theId -> theId) - Define.Field("name", StringType, "the member's name", fun _ (x : Member) -> x.Name) - ] - ) - - let meAsAMemberDef = - Define.Object( - name = nameof MeAsAMember, - description = "An organization member", - isTypeOf = (fun o -> o :? MeAsAMember), - fieldsFn = fun () -> [ - Define.Field("privId", GuidType, "the member's private ID used for authenticating their requests", fun _ (x : MeAsAMember) -> match x.PrivId with MemberPrivateId theId -> theId) - Define.Field("id", GuidType, "the member's ID", fun _ (x : MeAsAMember) -> match x.Id with MemberId theId -> theId) - Define.Field("name", StringType, "the member's name", fun _ (x : MeAsAMember) -> x.Name) - ] - ) - - let chatMemberDef = - Define.Object( - name = nameof ChatMember, - description = "A chat member is an organization member participating in a chat room", - isTypeOf = (fun o -> o :? ChatMember), - fieldsFn = fun () -> [ - Define.Field("id", GuidType, "the member's ID", fun _ (x : ChatMember) -> match x.Id with MemberId theId -> theId) - Define.Field("name", StringType, "the member's name", fun _ (x : ChatMember) -> x.Name) - Define.Field("role", memberRoleInChatEnumDef, "the member's role in the chat", fun _ (x : ChatMember) -> x.Role) - ] - ) - - let meAsAChatMemberDef = - Define.Object( - name = nameof MeAsAChatMember, - description = "A chat member is an organization member participating in a chat room", - isTypeOf = (fun o -> o :? MeAsAChatMember), - fieldsFn = fun () -> [ - Define.Field("privId", GuidType, "the member's private ID used for authenticating their requests", fun _ (x : MeAsAChatMember) -> match x.PrivId with MemberPrivateId theId -> theId) - Define.Field("id", GuidType, "the member's ID", fun _ (x : MeAsAChatMember) -> match x.Id with MemberId theId -> theId) - Define.Field("name", StringType, "the member's name", fun _ (x : MeAsAChatMember) -> x.Name) - Define.Field("role", memberRoleInChatEnumDef, "the member's role in the chat", fun _ (x : MeAsAChatMember) -> x.Role) - ] - ) - - let chatRoomStatsDef = - Define.Object( - name = nameof ChatRoom, - description = "A chat room as viewed from the outside", - isTypeOf = (fun o -> o :? ChatRoom), - fieldsFn = fun () -> [ - Define.Field("id", GuidType, "the chat room's ID", fun _ (x : ChatRoom) -> match x.Id with ChatRoomId theId -> theId) - Define.Field("name", StringType, "the chat room's name", fun _ (x : ChatRoom) -> x.Name) - Define.Field("members", ListOf chatMemberDef, "the members in the chat room", fun _ (x : ChatRoom) -> x.Members) - ] - ) - - let chatRoomDetailsDef = - Define.Object( - name = nameof ChatRoomForMember, - description = "A chat room as viewed by a chat room member", - isTypeOf = (fun o -> o :? ChatRoomForMember), - fieldsFn = fun () -> [ - Define.Field("id", GuidType, "the chat room's ID", fun _ (x : ChatRoomForMember) -> match x.Id with ChatRoomId theId -> theId) - Define.Field("name", StringType, "the chat room's name", fun _ (x : ChatRoomForMember) -> x.Name) - Define.Field("meAsAChatMember", meAsAChatMemberDef, "the chat member that queried the details", fun _ (x : ChatRoomForMember) -> x.MeAsAChatMember) - Define.Field("otherChatMembers", ListOf chatMemberDef, "the chat members excluding the one who queried the details", fun _ (x : ChatRoomForMember) -> x.OtherChatMembers) - ] - ) - - let organizationStatsDef = - Define.Object( - name = nameof Organization, - description = "An organization as seen from the outside", - isTypeOf = (fun o -> o :? Organization), - fieldsFn = fun () -> [ - Define.Field("id", GuidType, "the organization's ID", fun _ (x : Organization) -> match x.Id with OrganizationId theId -> theId) - Define.Field("name", StringType, "the organization's name", fun _ (x : Organization) -> x.Name) - Define.Field("members", ListOf memberDef, "members of this organization", fun _ (x : Organization) -> x.Members) - Define.Field("chatRooms", ListOf chatRoomStatsDef, "chat rooms in this organization", fun _ (x : Organization) -> x.ChatRooms) - ] - ) - - let organizationDetailsDef = - Define.Object( - name = nameof OrganizationForMember, - description = "An organization as seen by one of the organization's members", - isTypeOf = (fun o -> o :? OrganizationForMember), - fieldsFn = fun () -> [ - Define.Field("id", GuidType, "the organization's ID", fun _ (x : OrganizationForMember) -> match x.Id with OrganizationId theId -> theId) - Define.Field("name", StringType, "the organization's name", fun _ (x : OrganizationForMember) -> x.Name) - Define.Field("meAsAMember", meAsAMemberDef, "the member that queried the details", fun _ (x : OrganizationForMember) -> x.MeAsAMember) - Define.Field("otherMembers", ListOf memberDef, "members of this organization", fun _ (x : OrganizationForMember) -> x.OtherMembers) - Define.Field("chatRooms", ListOf chatRoomStatsDef, "chat rooms in this organization", fun _ (x : OrganizationForMember) -> x.ChatRooms) - ] - ) - - let aChatRoomMessageDef description name = - Define.Object( - name = name, - description = description, - isTypeOf = (fun o -> o :? ChatRoomMessage), - fieldsFn = fun () -> [ - Define.Field("id", GuidType, "the message's ID", fun _ (x : ChatRoomMessage) -> match x.Id with MessageId theId -> theId) - Define.Field("chatRoomId", GuidType, "the ID of the chat room the message belongs to", fun _ (x : ChatRoomMessage) -> match x.ChatRoomId with ChatRoomId theId -> theId) - Define.Field("date", DateTimeOffsetType, "the time the message was received at the server", fun _ (x : ChatRoomMessage) -> DateTimeOffset(x.Date, TimeSpan.Zero)) - Define.Field("authorId", GuidType, "the member ID of the message's author", fun _ (x : ChatRoomMessage) -> match x.AuthorId with MemberId theId -> theId) - Define.Field("text", StringType, "the message's text", fun _ (x : ChatRoomMessage) -> x.Text) - ] - ) - - let anEmptyChatRoomEvent description name = - Define.Object( - name = name, - description = description, - isTypeOf = (fun o -> o :? unit), - fieldsFn = fun () -> [ - Define.Field("doNotUse", BooleanType, "this is just to satify the expected structure of this type", fun _ _ -> true) - ] - ) - - let aChatRoomEventForMessageId description name = - Define.Object( - name = name, - description = description, - isTypeOf = (fun o -> o :? MessageId), - fieldsFn = (fun () -> [ - Define.Field("messageId", GuidType, "this is the message ID", fun _ (x : MessageId) -> match x with MessageId theId -> theId) - ]) - ) - - let aChatRoomEventForMemberIdAndName description name = - Define.Object( - name = name, - description = description, - isTypeOf = (fun o -> o :? (MemberId * string)), - fieldsFn = (fun () -> [ - Define.Field("memberId", GuidType, "this is the member's ID", fun _ (mId : MemberId, _ : string) -> match mId with MemberId theId -> theId) - Define.Field("memberName", StringType, "this is the member's name", fun _ (_ : MemberId, name : string) -> name) - ]) - ) - - let newMessageDef = nameof NewMessage |> aChatRoomMessageDef "a new public message has been sent in the chat room" - let editedMessageDef = nameof EditedMessage |> aChatRoomMessageDef "a public message of the chat room has been edited" - let deletedMessageDef = nameof DeletedMessage |> aChatRoomEventForMessageId "a public message of the chat room has been deleted" - let memberJoinedDef = nameof MemberJoined |> aChatRoomEventForMemberIdAndName "a member has joined the chat" - let memberLeftDef = nameof MemberLeft |> aChatRoomEventForMemberIdAndName "a member has left the chat" - - let chatRoomSpecificEventDef = - Define.Union( - name = nameof ChatRoomSpecificEvent, - options = - [ newMessageDef - editedMessageDef - deletedMessageDef - memberJoinedDef - memberLeftDef ], - resolveValue = - (fun o -> - match o with - | NewMessage x -> box x - | EditedMessage x -> upcast x - | DeletedMessage x -> upcast x - | MemberJoined (mId, mName) -> upcast (mId, mName) - | MemberLeft (mId, mName) -> upcast (mId, mName) - ), - resolveType = - (fun o -> - match o with - | NewMessage _ -> newMessageDef - | EditedMessage _ -> editedMessageDef - | DeletedMessage _ -> deletedMessageDef - | MemberJoined _ -> memberJoinedDef - | MemberLeft _ -> memberLeftDef - ), - description = "data which is specific to a certain type of event" - ) - - let chatRoomEventDef = - Define.Object( - name = nameof ChatRoomEvent, - description = "Something that happened in the chat room, like a new message sent", - isTypeOf = (fun o -> o :? ChatRoomEvent), - fieldsFn = (fun () -> [ - Define.Field("chatRoomId", GuidType, "the ID of the chat room in which the event happened", fun _ (x : ChatRoomEvent) -> match x.ChatRoomId with ChatRoomId theId -> theId) - Define.Field("time", DateTimeOffsetType, "the time the message was received at the server", fun _ (x : ChatRoomEvent) -> DateTimeOffset(x.Time, TimeSpan.Zero)) - Define.Field("specificData", chatRoomSpecificEventDef, "the event's specific data", fun _ (x : ChatRoomEvent) -> x.SpecificData) - ]) - ) - - let query = - Define.Object( - name = "Query", - fields = [ - Define.Field( - "organizations", - ListOf organizationStatsDef, - "gets all available organizations", - fun _ _ -> - FakePersistence.Organizations.Values - |> Seq.map MapFrom.organizationInDb_To_Organization - |> List.ofSeq + let authenticateMemberInOrganization + (organizationId : OrganizationId) + (memberPrivId : MemberPrivateId) + : Result<(Organization_In_Db * Member_In_Db), GraphQLException> = + let maybeOrganization = FakePersistence.Organizations |> Map.tryFind organizationId + let maybeMember = + FakePersistence.Members.Values + |> Seq.tryFind (fun x -> x.PrivId = memberPrivId) + + match (maybeOrganization, maybeMember) with + | None, _ -> Error (organizationId |> Exceptions.Organization_Doesnt_Exist) + | _, None -> Error (memberPrivId |> Exceptions.PrivMember_Doesnt_Exist) + | Some organization, Some theMember -> + if not (organization.Members |> List.contains theMember.Id) then + Error (Exceptions.Member_Isnt_Part_Of_Org ()) + else + Ok (organization, theMember) + + let validateChatRoomExistence (organization : Organization_In_Db) (chatRoomId : ChatRoomId) : Result = + match FakePersistence.ChatRooms |> Map.tryFind chatRoomId with + | None -> Error <| Exceptions.ChatRoom_Doesnt_Exist chatRoomId + | Some chatRoom -> + if not (organization.ChatRooms |> List.contains chatRoom.Id) then + Error <| Exceptions.ChatRoom_Isnt_Part_Of_Org () + else + Ok chatRoom + + let validateMessageExistence (chatRoom : ChatRoom_In_Db) (messageId : MessageId) : Result = + match + FakePersistence.ChatRoomMessages + |> Map.tryFind (chatRoom.Id, messageId) + with + | None -> Error (GQLMessageException ("chat message doesn't exist (anymore)")) + | Some chatMessage -> Ok chatMessage + + let succeedOrRaiseGraphQLEx<'T> (result : Result<'T, GraphQLException>) : 'T = + match result with + | Error ex -> raise ex + | Ok s -> s + + let chatRoomEvents_subscription_name = "chatRoomEvents" + + let memberRoleInChatEnumDef = + Define.Enum ( + name = nameof MemberRoleInChat, + options = [ + Define.EnumValue (ChatAdmin.ToString (), ChatAdmin) + Define.EnumValue (ChatGuest.ToString (), ChatGuest) + ] ) - ] - ) - - let schemaConfig = SchemaConfig.Default - - let publishChatRoomEvent (specificEvent : ChatRoomSpecificEvent) (chatRoomId : ChatRoomId) : unit = - { ChatRoomId = chatRoomId - Time = DateTime.UtcNow - SpecificData = specificEvent } - |> schemaConfig.SubscriptionProvider.Publish chatRoomEvents_subscription_name - - let mutation = - Define.Object( - name = "Mutation", - fields = [ - Define.Field( - "enterOrganization", - organizationDetailsDef, - "makes a new member enter an organization", - [ Define.Input ("organizationId", GuidType, description = "the ID of the organization") - Define.Input ("member", StringType, description = "the new member's name") - ], - fun ctx root -> - let organizationId = OrganizationId (ctx.Arg("organizationId")) - let newMemberName : string = ctx.Arg("member") - let maybeResult = - FakePersistence.Organizations - |> Map.tryFind organizationId - |> Option.map MapFrom.organizationInDb_To_Organization - |> Option.map - (fun organization -> - if organization.Members |> List.exists (fun m -> m.Name = newMemberName) then - raise (newMemberName |> Exceptions.Member_With_This_Name_Already_Exists) - else - let newMemberPrivId = MemberPrivateId (Guid.NewGuid()) - let newMemberId = MemberId (Guid.NewGuid()) - let newMember = - { Member_In_Db.PrivId = newMemberPrivId - Id = newMemberId - Name = newMemberName } - FakePersistence.Members <- - FakePersistence.Members - |> Map.add newMemberId newMember - - FakePersistence.Organizations <- - FakePersistence.Organizations - |> Map.change organizationId - (Option.bind - (fun organization -> - Some { organization with Members = newMemberId :: organization.Members } - ) - ) - FakePersistence.Organizations - |> Map.find organizationId - |> MapFrom.organizationInDb_To_OrganizationForMember newMemberId - ) - |> Option.flatten - match maybeResult with - | None -> - raise (GQLMessageException("couldn't enter organization (maybe the ID is incorrect?)")) - | Some res -> - res + + let memberDef = + Define.Object ( + name = nameof Member, + description = "An organization member", + isTypeOf = (fun o -> o :? Member), + fieldsFn = + fun () -> [ + Define.Field ( + "id", + GuidType, + "the member's ID", + fun _ (x : Member) -> + match x.Id with + | MemberId theId -> theId + ) + Define.Field ("name", StringType, "the member's name", (fun _ (x : Member) -> x.Name)) + ] ) - Define.Field( - "createChatRoom", - chatRoomDetailsDef, - "creates a new chat room for a user", - [ Define.Input ("organizationId", GuidType, description = "the ID of the organization in which the chat room will be created") - Define.Input ("memberId", GuidType, description = "the member's private ID") - Define.Input ("name", StringType, description = "the chat room's name") - ], - fun ctx root -> - let organizationId = OrganizationId (ctx.Arg("organizationId")) - let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) - let chatRoomName : string = ctx.Arg("name") - - memberPrivId - |> authenticateMemberInOrganization organizationId - |> Result.map - (fun (organization, theMember) -> - let newChatRoomId = ChatRoomId (Guid.NewGuid()) - let newChatMember : ChatMember_In_Db = - { ChatRoomId = newChatRoomId - MemberId = theMember.Id - Role = ChatAdmin } - let newChatRoom : ChatRoom_In_Db = - { Id = newChatRoomId - Name = chatRoomName - Members = [ theMember.Id ] } - FakePersistence.ChatRooms <- - FakePersistence.ChatRooms |> Map.add newChatRoomId newChatRoom - FakePersistence.ChatMembers <- - FakePersistence.ChatMembers |> Map.add (newChatRoomId, theMember.Id) newChatMember - FakePersistence.Organizations <- - FakePersistence.Organizations - |> Map.change - organizationId - (Option.map(fun org -> { org with ChatRooms = newChatRoomId :: org.ChatRooms })) - - MapFrom.chatRoomInDb_To_ChatRoomForMember - (FakePersistence.Members.Values |> Seq.filter (fun x -> organization.Members |> List.contains x.Id)) - newChatMember - newChatRoom - ) - |> succeedOrRaiseGraphQLEx + + let meAsAMemberDef = + Define.Object ( + name = nameof MeAsAMember, + description = "An organization member", + isTypeOf = (fun o -> o :? MeAsAMember), + fieldsFn = + fun () -> [ + Define.Field ( + "privId", + GuidType, + "the member's private ID used for authenticating their requests", + fun _ (x : MeAsAMember) -> + match x.PrivId with + | MemberPrivateId theId -> theId + ) + Define.Field ( + "id", + GuidType, + "the member's ID", + fun _ (x : MeAsAMember) -> + match x.Id with + | MemberId theId -> theId + ) + Define.Field ("name", StringType, "the member's name", (fun _ (x : MeAsAMember) -> x.Name)) + ] ) - Define.Field( - "enterChatRoom", - chatRoomDetailsDef, - "makes a member enter a chat room", - [ Define.Input ("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") - Define.Input ("chatRoomId", GuidType, description = "the ID of the chat room") - Define.Input ("memberId", GuidType, description = "the member's private ID") - ], - fun ctx root -> - let organizationId = OrganizationId (ctx.Arg("organizationId")) - let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) - let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) - - memberPrivId - |> authenticateMemberInOrganization organizationId - |> Result.bind - (fun (organization, theMember) -> - chatRoomId - |> validateChatRoomExistence organization - |> Result.map (fun chatRoom -> (organization, chatRoom, theMember)) - ) - |> Result.map - (fun (_, chatRoom, theMember) -> - let newChatMember : ChatMember_In_Db = - { ChatRoomId = chatRoom.Id - MemberId = theMember.Id - Role = ChatGuest - } - FakePersistence.ChatMembers <- - FakePersistence.ChatMembers - |> Map.add - (newChatMember.ChatRoomId, newChatMember.MemberId) - newChatMember - FakePersistence.ChatRooms <- - FakePersistence.ChatRooms - |> Map.change - chatRoom.Id - (Option.map (fun theChatRoom -> { theChatRoom with Members = newChatMember.MemberId :: theChatRoom.Members})) - let theChatRoom = FakePersistence.ChatRooms |> Map.find chatRoomId - let result = - MapFrom.chatRoomInDb_To_ChatRoomForMember - (FakePersistence.Members.Values - |> Seq.filter - (fun x -> theChatRoom.Members |> List.contains x.Id) - ) - newChatMember - theChatRoom - - chatRoom.Id - |> publishChatRoomEvent (MemberJoined (theMember.Id, theMember.Name)) - - result + + let chatMemberDef = + Define.Object ( + name = nameof ChatMember, + description = "A chat member is an organization member participating in a chat room", + isTypeOf = (fun o -> o :? ChatMember), + fieldsFn = + fun () -> [ + Define.Field ( + "id", + GuidType, + "the member's ID", + fun _ (x : ChatMember) -> + match x.Id with + | MemberId theId -> theId + ) + Define.Field ("name", StringType, "the member's name", (fun _ (x : ChatMember) -> x.Name)) + Define.Field ("role", memberRoleInChatEnumDef, "the member's role in the chat", (fun _ (x : ChatMember) -> x.Role)) + ] + ) + + let meAsAChatMemberDef = + Define.Object ( + name = nameof MeAsAChatMember, + description = "A chat member is an organization member participating in a chat room", + isTypeOf = (fun o -> o :? MeAsAChatMember), + fieldsFn = + fun () -> [ + Define.Field ( + "privId", + GuidType, + "the member's private ID used for authenticating their requests", + fun _ (x : MeAsAChatMember) -> + match x.PrivId with + | MemberPrivateId theId -> theId + ) + Define.Field ( + "id", + GuidType, + "the member's ID", + fun _ (x : MeAsAChatMember) -> + match x.Id with + | MemberId theId -> theId + ) + Define.Field ("name", StringType, "the member's name", (fun _ (x : MeAsAChatMember) -> x.Name)) + Define.Field ("role", memberRoleInChatEnumDef, "the member's role in the chat", (fun _ (x : MeAsAChatMember) -> x.Role)) + ] + ) + + let chatRoomStatsDef = + Define.Object ( + name = nameof ChatRoom, + description = "A chat room as viewed from the outside", + isTypeOf = (fun o -> o :? ChatRoom), + fieldsFn = + fun () -> [ + Define.Field ( + "id", + GuidType, + "the chat room's ID", + fun _ (x : ChatRoom) -> + match x.Id with + | ChatRoomId theId -> theId + ) + Define.Field ("name", StringType, "the chat room's name", (fun _ (x : ChatRoom) -> x.Name)) + Define.Field ("members", ListOf chatMemberDef, "the members in the chat room", (fun _ (x : ChatRoom) -> x.Members)) + ] + ) + + let chatRoomDetailsDef = + Define.Object ( + name = nameof ChatRoomForMember, + description = "A chat room as viewed by a chat room member", + isTypeOf = (fun o -> o :? ChatRoomForMember), + fieldsFn = + fun () -> [ + Define.Field ( + "id", + GuidType, + "the chat room's ID", + fun _ (x : ChatRoomForMember) -> + match x.Id with + | ChatRoomId theId -> theId + ) + Define.Field ("name", StringType, "the chat room's name", (fun _ (x : ChatRoomForMember) -> x.Name)) + Define.Field ( + "meAsAChatMember", + meAsAChatMemberDef, + "the chat member that queried the details", + fun _ (x : ChatRoomForMember) -> x.MeAsAChatMember + ) + Define.Field ( + "otherChatMembers", + ListOf chatMemberDef, + "the chat members excluding the one who queried the details", + fun _ (x : ChatRoomForMember) -> x.OtherChatMembers + ) + ] + ) + + let organizationStatsDef = + Define.Object ( + name = nameof Organization, + description = "An organization as seen from the outside", + isTypeOf = (fun o -> o :? Organization), + fieldsFn = + fun () -> [ + Define.Field ( + "id", + GuidType, + "the organization's ID", + fun _ (x : Organization) -> + match x.Id with + | OrganizationId theId -> theId + ) + Define.Field ("name", StringType, "the organization's name", (fun _ (x : Organization) -> x.Name)) + Define.Field ("members", ListOf memberDef, "members of this organization", (fun _ (x : Organization) -> x.Members)) + Define.Field ("chatRooms", ListOf chatRoomStatsDef, "chat rooms in this organization", (fun _ (x : Organization) -> x.ChatRooms)) + ] + ) + + let organizationDetailsDef = + Define.Object ( + name = nameof OrganizationForMember, + description = "An organization as seen by one of the organization's members", + isTypeOf = (fun o -> o :? OrganizationForMember), + fieldsFn = + fun () -> [ + Define.Field ( + "id", + GuidType, + "the organization's ID", + fun _ (x : OrganizationForMember) -> + match x.Id with + | OrganizationId theId -> theId + ) + Define.Field ("name", StringType, "the organization's name", (fun _ (x : OrganizationForMember) -> x.Name)) + Define.Field ( + "meAsAMember", + meAsAMemberDef, + "the member that queried the details", + fun _ (x : OrganizationForMember) -> x.MeAsAMember + ) + Define.Field ( + "otherMembers", + ListOf memberDef, + "members of this organization", + fun _ (x : OrganizationForMember) -> x.OtherMembers + ) + Define.Field ( + "chatRooms", + ListOf chatRoomStatsDef, + "chat rooms in this organization", + fun _ (x : OrganizationForMember) -> x.ChatRooms + ) + ] + ) + + let aChatRoomMessageDef description name = + Define.Object ( + name = name, + description = description, + isTypeOf = (fun o -> o :? ChatRoomMessage), + fieldsFn = + fun () -> [ + Define.Field ( + "id", + GuidType, + "the message's ID", + fun _ (x : ChatRoomMessage) -> + match x.Id with + | MessageId theId -> theId + ) + Define.Field ( + "chatRoomId", + GuidType, + "the ID of the chat room the message belongs to", + fun _ (x : ChatRoomMessage) -> + match x.ChatRoomId with + | ChatRoomId theId -> theId + ) + Define.Field ( + "date", + DateTimeOffsetType, + "the time the message was received at the server", + fun _ (x : ChatRoomMessage) -> DateTimeOffset (x.Date, TimeSpan.Zero) + ) + Define.Field ( + "authorId", + GuidType, + "the member ID of the message's author", + fun _ (x : ChatRoomMessage) -> + match x.AuthorId with + | MemberId theId -> theId + ) + Define.Field ("text", StringType, "the message's text", (fun _ (x : ChatRoomMessage) -> x.Text)) + ] + ) + + let anEmptyChatRoomEvent description name = + Define.Object ( + name = name, + description = description, + isTypeOf = (fun o -> o :? unit), + fieldsFn = + fun () -> [ + Define.Field ("doNotUse", BooleanType, "this is just to satify the expected structure of this type", (fun _ _ -> true)) + ] + ) + + let aChatRoomEventForMessageId description name = + Define.Object ( + name = name, + description = description, + isTypeOf = (fun o -> o :? MessageId), + fieldsFn = + (fun () -> [ + Define.Field ( + "messageId", + GuidType, + "this is the message ID", + fun _ (x : MessageId) -> + match x with + | MessageId theId -> theId + ) + ]) + ) + + let aChatRoomEventForMemberIdAndName description name = + Define.Object ( + name = name, + description = description, + isTypeOf = (fun o -> o :? (MemberId * string)), + fieldsFn = + (fun () -> [ + Define.Field ( + "memberId", + GuidType, + "this is the member's ID", + fun _ (mId : MemberId, _ : string) -> + match mId with + | MemberId theId -> theId + ) + Define.Field ("memberName", StringType, "this is the member's name", (fun _ (_ : MemberId, name : string) -> name)) + ]) + ) + + let newMessageDef = + nameof NewMessage + |> aChatRoomMessageDef "a new public message has been sent in the chat room" + let editedMessageDef = + nameof EditedMessage + |> aChatRoomMessageDef "a public message of the chat room has been edited" + let deletedMessageDef = + nameof DeletedMessage + |> aChatRoomEventForMessageId "a public message of the chat room has been deleted" + let memberJoinedDef = + nameof MemberJoined + |> aChatRoomEventForMemberIdAndName "a member has joined the chat" + let memberLeftDef = + nameof MemberLeft + |> aChatRoomEventForMemberIdAndName "a member has left the chat" + + let chatRoomSpecificEventDef = + Define.Union ( + name = nameof ChatRoomSpecificEvent, + options = [ newMessageDef; editedMessageDef; deletedMessageDef; memberJoinedDef; memberLeftDef ], + resolveValue = + (fun o -> + match o with + | NewMessage x -> box x + | EditedMessage x -> upcast x + | DeletedMessage x -> upcast x + | MemberJoined (mId, mName) -> upcast (mId, mName) + | MemberLeft (mId, mName) -> upcast (mId, mName)), + resolveType = + (fun o -> + match o with + | NewMessage _ -> newMessageDef + | EditedMessage _ -> editedMessageDef + | DeletedMessage _ -> deletedMessageDef + | MemberJoined _ -> memberJoinedDef + | MemberLeft _ -> memberLeftDef), + description = "data which is specific to a certain type of event" + ) + + let chatRoomEventDef = + Define.Object ( + name = nameof ChatRoomEvent, + description = "Something that happened in the chat room, like a new message sent", + isTypeOf = (fun o -> o :? ChatRoomEvent), + fieldsFn = + (fun () -> [ + Define.Field ( + "chatRoomId", + GuidType, + "the ID of the chat room in which the event happened", + fun _ (x : ChatRoomEvent) -> + match x.ChatRoomId with + | ChatRoomId theId -> theId + ) + Define.Field ( + "time", + DateTimeOffsetType, + "the time the message was received at the server", + fun _ (x : ChatRoomEvent) -> DateTimeOffset (x.Time, TimeSpan.Zero) + ) + Define.Field ( + "specificData", + chatRoomSpecificEventDef, + "the event's specific data", + fun _ (x : ChatRoomEvent) -> x.SpecificData + ) + ]) + ) + + let query = + Define.Object ( + name = "Query", + fields = [ + Define.Field ( + "organizations", + ListOf organizationStatsDef, + "gets all available organizations", + fun _ _ -> + FakePersistence.Organizations.Values + |> Seq.map MapFrom.organizationInDb_To_Organization + |> List.ofSeq ) - |> succeedOrRaiseGraphQLEx + ] ) - Define.Field( - "leaveChatRoom", - BooleanType, - "makes a member leave a chat room", - [ Define.Input ("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") - Define.Input ("chatRoomId", GuidType, description = "the ID of the chat room") - Define.Input ("memberId", GuidType, description = "the member's private ID") - ], - fun ctx root -> - let organizationId = OrganizationId (ctx.Arg("organizationId")) - let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) - let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) - - memberPrivId - |> authenticateMemberInOrganization organizationId - |> Result.bind - (fun (organization, theMember) -> - chatRoomId - |> validateChatRoomExistence organization - |> Result.map (fun chatRoom -> (organization, chatRoom, theMember)) + + let schemaConfig = SchemaConfig.Default + + let publishChatRoomEvent (specificEvent : ChatRoomSpecificEvent) (chatRoomId : ChatRoomId) : unit = + { + ChatRoomId = chatRoomId + Time = DateTime.UtcNow + SpecificData = specificEvent + } + |> schemaConfig.SubscriptionProvider.Publish chatRoomEvents_subscription_name + + let mutation = + Define.Object ( + name = "Mutation", + fields = [ + Define.Field ( + "enterOrganization", + organizationDetailsDef, + "makes a new member enter an organization", + [ + Define.Input ("organizationId", GuidType, description = "the ID of the organization") + Define.Input ("member", StringType, description = "the new member's name") + ], + fun ctx root -> + let organizationId = OrganizationId (ctx.Arg ("organizationId")) + let newMemberName : string = ctx.Arg ("member") + let maybeResult = + FakePersistence.Organizations + |> Map.tryFind organizationId + |> Option.map MapFrom.organizationInDb_To_Organization + |> Option.map (fun organization -> + if + organization.Members + |> List.exists (fun m -> m.Name = newMemberName) + then + raise ( + newMemberName + |> Exceptions.Member_With_This_Name_Already_Exists + ) + else + let newMemberPrivId = MemberPrivateId (Guid.NewGuid ()) + let newMemberId = MemberId (Guid.NewGuid ()) + let newMember = { + Member_In_Db.PrivId = newMemberPrivId + Id = newMemberId + Name = newMemberName + } + FakePersistence.Members <- FakePersistence.Members |> Map.add newMemberId newMember + + FakePersistence.Organizations <- + FakePersistence.Organizations + |> Map.change + organizationId + (Option.bind (fun organization -> + Some { organization with Members = newMemberId :: organization.Members })) + FakePersistence.Organizations + |> Map.find organizationId + |> MapFrom.organizationInDb_To_OrganizationForMember newMemberId) + |> Option.flatten + match maybeResult with + | None -> raise (GQLMessageException ("couldn't enter organization (maybe the ID is incorrect?)")) + | Some res -> res ) - |> Result.map - (fun (_, chatRoom, theMember) -> - FakePersistence.ChatMembers <- - FakePersistence.ChatMembers |> Map.remove (chatRoom.Id, theMember.Id) - FakePersistence.ChatRooms <- - FakePersistence.ChatRooms - |> Map.change - chatRoom.Id - (Option.map (fun theChatRoom -> { theChatRoom with Members = theChatRoom.Members |> List.filter (fun mId -> mId <> theMember.Id)})) - true + Define.Field ( + "createChatRoom", + chatRoomDetailsDef, + "creates a new chat room for a user", + [ + Define.Input ("organizationId", GuidType, description = "the ID of the organization in which the chat room will be created") + Define.Input ("memberId", GuidType, description = "the member's private ID") + Define.Input ("name", StringType, description = "the chat room's name") + ], + fun ctx root -> + let organizationId = OrganizationId (ctx.Arg ("organizationId")) + let memberPrivId = MemberPrivateId (ctx.Arg ("memberId")) + let chatRoomName : string = ctx.Arg ("name") + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> Result.map (fun (organization, theMember) -> + let newChatRoomId = ChatRoomId (Guid.NewGuid ()) + let newChatMember : ChatMember_In_Db = { ChatRoomId = newChatRoomId; MemberId = theMember.Id; Role = ChatAdmin } + let newChatRoom : ChatRoom_In_Db = { Id = newChatRoomId; Name = chatRoomName; Members = [ theMember.Id ] } + FakePersistence.ChatRooms <- + FakePersistence.ChatRooms + |> Map.add newChatRoomId newChatRoom + FakePersistence.ChatMembers <- + FakePersistence.ChatMembers + |> Map.add (newChatRoomId, theMember.Id) newChatMember + FakePersistence.Organizations <- + FakePersistence.Organizations + |> Map.change organizationId (Option.map (fun org -> { org with ChatRooms = newChatRoomId :: org.ChatRooms })) + + MapFrom.chatRoomInDb_To_ChatRoomForMember + (FakePersistence.Members.Values + |> Seq.filter (fun x -> organization.Members |> List.contains x.Id)) + newChatMember + newChatRoom) + |> succeedOrRaiseGraphQLEx ) - |> succeedOrRaiseGraphQLEx - ) - Define.Field( - "sendChatMessage", - BooleanType, - [ Define.Input("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") - Define.Input("chatRoomId", GuidType, description = "the chat room's ID") - Define.Input("memberId", GuidType, description = "the member's private ID") - Define.Input("text", StringType, description = "the chat message's contents")], - fun ctx _ -> - let organizationId = OrganizationId (ctx.Arg("organizationId")) - let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) - let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) - let text : string = ctx.Arg("text") - - memberPrivId - |> authenticateMemberInOrganization organizationId - |> Result.bind - (fun (organization, theMember) -> - chatRoomId - |> validateChatRoomExistence organization - |> Result.map (fun chatRoom -> (organization, chatRoom, theMember)) + Define.Field ( + "enterChatRoom", + chatRoomDetailsDef, + "makes a member enter a chat room", + [ + Define.Input ("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") + Define.Input ("chatRoomId", GuidType, description = "the ID of the chat room") + Define.Input ("memberId", GuidType, description = "the member's private ID") + ], + fun ctx root -> + let organizationId = OrganizationId (ctx.Arg ("organizationId")) + let chatRoomId = ChatRoomId (ctx.Arg ("chatRoomId")) + let memberPrivId = MemberPrivateId (ctx.Arg ("memberId")) + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> Result.bind (fun (organization, theMember) -> + chatRoomId + |> validateChatRoomExistence organization + |> Result.map (fun chatRoom -> (organization, chatRoom, theMember))) + |> Result.map (fun (_, chatRoom, theMember) -> + let newChatMember : ChatMember_In_Db = { ChatRoomId = chatRoom.Id; MemberId = theMember.Id; Role = ChatGuest } + FakePersistence.ChatMembers <- + FakePersistence.ChatMembers + |> Map.add (newChatMember.ChatRoomId, newChatMember.MemberId) newChatMember + FakePersistence.ChatRooms <- + FakePersistence.ChatRooms + |> Map.change + chatRoom.Id + (Option.map (fun theChatRoom -> { theChatRoom with Members = newChatMember.MemberId :: theChatRoom.Members })) + let theChatRoom = FakePersistence.ChatRooms |> Map.find chatRoomId + let result = + MapFrom.chatRoomInDb_To_ChatRoomForMember + (FakePersistence.Members.Values + |> Seq.filter (fun x -> theChatRoom.Members |> List.contains x.Id)) + newChatMember + theChatRoom + + chatRoom.Id + |> publishChatRoomEvent (MemberJoined (theMember.Id, theMember.Name)) + + result) + |> succeedOrRaiseGraphQLEx ) - |> Result.map - (fun (_, chatRoom, theMember) -> - let newChatRoomMessage = - { Id = MessageId (Guid.NewGuid()) - ChatRoomId = chatRoom.Id - Date = DateTime.UtcNow - AuthorId = theMember.Id - Text = text } - FakePersistence.ChatRoomMessages <- - FakePersistence.ChatRoomMessages - |> Map.add - (chatRoom.Id, newChatRoomMessage.Id) - newChatRoomMessage - - chatRoom.Id - |> publishChatRoomEvent (NewMessage newChatRoomMessage) - - true + Define.Field ( + "leaveChatRoom", + BooleanType, + "makes a member leave a chat room", + [ + Define.Input ("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") + Define.Input ("chatRoomId", GuidType, description = "the ID of the chat room") + Define.Input ("memberId", GuidType, description = "the member's private ID") + ], + fun ctx root -> + let organizationId = OrganizationId (ctx.Arg ("organizationId")) + let chatRoomId = ChatRoomId (ctx.Arg ("chatRoomId")) + let memberPrivId = MemberPrivateId (ctx.Arg ("memberId")) + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> Result.bind (fun (organization, theMember) -> + chatRoomId + |> validateChatRoomExistence organization + |> Result.map (fun chatRoom -> (organization, chatRoom, theMember))) + |> Result.map (fun (_, chatRoom, theMember) -> + FakePersistence.ChatMembers <- + FakePersistence.ChatMembers + |> Map.remove (chatRoom.Id, theMember.Id) + FakePersistence.ChatRooms <- + FakePersistence.ChatRooms + |> Map.change + chatRoom.Id + (Option.map (fun theChatRoom -> { + theChatRoom with + Members = + theChatRoom.Members + |> List.filter (fun mId -> mId <> theMember.Id) + })) + true) + |> succeedOrRaiseGraphQLEx ) - |> succeedOrRaiseGraphQLEx - ) - Define.Field( - "editChatMessage", - BooleanType, - [ Define.Input("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") - Define.Input("chatRoomId", GuidType, description = "the chat room's ID") - Define.Input("memberId", GuidType, description = "the member's private ID") - Define.Input("messageId", GuidType, description = "the existing message's ID") - Define.Input("text", StringType, description = "the chat message's contents")], - fun ctx _ -> - let organizationId = OrganizationId (ctx.Arg("organizationId")) - let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) - let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) - let messageId = MessageId (ctx.Arg("messageId")) - let text : string = ctx.Arg("text") - - memberPrivId - |> authenticateMemberInOrganization organizationId - |> Result.bind - (fun (organization, theMember) -> - chatRoomId - |> validateChatRoomExistence organization - |> Result.bind (fun chatRoom -> messageId |> validateMessageExistence chatRoom |> Result.map (fun x -> (chatRoom, x))) - |> Result.map (fun (chatRoom, chatMessage) -> (organization, chatRoom, theMember, chatMessage)) + Define.Field ( + "sendChatMessage", + BooleanType, + [ + Define.Input ("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") + Define.Input ("chatRoomId", GuidType, description = "the chat room's ID") + Define.Input ("memberId", GuidType, description = "the member's private ID") + Define.Input ("text", StringType, description = "the chat message's contents") + ], + fun ctx _ -> + let organizationId = OrganizationId (ctx.Arg ("organizationId")) + let chatRoomId = ChatRoomId (ctx.Arg ("chatRoomId")) + let memberPrivId = MemberPrivateId (ctx.Arg ("memberId")) + let text : string = ctx.Arg ("text") + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> Result.bind (fun (organization, theMember) -> + chatRoomId + |> validateChatRoomExistence organization + |> Result.map (fun chatRoom -> (organization, chatRoom, theMember))) + |> Result.map (fun (_, chatRoom, theMember) -> + let newChatRoomMessage = { + Id = MessageId (Guid.NewGuid ()) + ChatRoomId = chatRoom.Id + Date = DateTime.UtcNow + AuthorId = theMember.Id + Text = text + } + FakePersistence.ChatRoomMessages <- + FakePersistence.ChatRoomMessages + |> Map.add (chatRoom.Id, newChatRoomMessage.Id) newChatRoomMessage + + chatRoom.Id + |> publishChatRoomEvent (NewMessage newChatRoomMessage) + + true) + |> succeedOrRaiseGraphQLEx ) - |> Result.map - (fun (_, chatRoom, theMember, chatMessage) -> - let newChatRoomMessage = - { Id = chatMessage.Id - ChatRoomId = chatRoom.Id - Date = chatMessage.Date - AuthorId = theMember.Id - Text = text } - FakePersistence.ChatRoomMessages <- - FakePersistence.ChatRoomMessages - |> Map.change - (chatRoom.Id, newChatRoomMessage.Id) - (Option.map (fun _ -> newChatRoomMessage)) - - chatRoom.Id - |> publishChatRoomEvent (EditedMessage newChatRoomMessage) - - true + Define.Field ( + "editChatMessage", + BooleanType, + [ + Define.Input ("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") + Define.Input ("chatRoomId", GuidType, description = "the chat room's ID") + Define.Input ("memberId", GuidType, description = "the member's private ID") + Define.Input ("messageId", GuidType, description = "the existing message's ID") + Define.Input ("text", StringType, description = "the chat message's contents") + ], + fun ctx _ -> + let organizationId = OrganizationId (ctx.Arg ("organizationId")) + let chatRoomId = ChatRoomId (ctx.Arg ("chatRoomId")) + let memberPrivId = MemberPrivateId (ctx.Arg ("memberId")) + let messageId = MessageId (ctx.Arg ("messageId")) + let text : string = ctx.Arg ("text") + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> Result.bind (fun (organization, theMember) -> + chatRoomId + |> validateChatRoomExistence organization + |> Result.bind (fun chatRoom -> + messageId + |> validateMessageExistence chatRoom + |> Result.map (fun x -> (chatRoom, x))) + |> Result.map (fun (chatRoom, chatMessage) -> (organization, chatRoom, theMember, chatMessage))) + |> Result.map (fun (_, chatRoom, theMember, chatMessage) -> + let newChatRoomMessage = { + Id = chatMessage.Id + ChatRoomId = chatRoom.Id + Date = chatMessage.Date + AuthorId = theMember.Id + Text = text + } + FakePersistence.ChatRoomMessages <- + FakePersistence.ChatRoomMessages + |> Map.change (chatRoom.Id, newChatRoomMessage.Id) (Option.map (fun _ -> newChatRoomMessage)) + + chatRoom.Id + |> publishChatRoomEvent (EditedMessage newChatRoomMessage) + + true) + |> succeedOrRaiseGraphQLEx ) - |> succeedOrRaiseGraphQLEx - ) - Define.Field( - "deleteChatMessage", - BooleanType, - [ Define.Input("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") - Define.Input("chatRoomId", GuidType, description = "the chat room's ID") - Define.Input("memberId", GuidType, description = "the member's private ID") - Define.Input("messageId", GuidType, description = "the existing message's ID")], - fun ctx _ -> - let organizationId = OrganizationId (ctx.Arg("organizationId")) - let chatRoomId = ChatRoomId (ctx.Arg("chatRoomId")) - let memberPrivId = MemberPrivateId (ctx.Arg("memberId")) - let messageId = MessageId (ctx.Arg("messageId")) - - memberPrivId - |> authenticateMemberInOrganization organizationId - |> Result.bind - (fun (organization, theMember) -> - chatRoomId - |> validateChatRoomExistence organization - |> Result.bind (fun chatRoom -> messageId |> validateMessageExistence chatRoom |> Result.map (fun x -> (chatRoom, x))) - |> Result.map (fun (chatRoom, chatMessage) -> (organization, chatRoom, theMember, chatMessage)) + Define.Field ( + "deleteChatMessage", + BooleanType, + [ + Define.Input ("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") + Define.Input ("chatRoomId", GuidType, description = "the chat room's ID") + Define.Input ("memberId", GuidType, description = "the member's private ID") + Define.Input ("messageId", GuidType, description = "the existing message's ID") + ], + fun ctx _ -> + let organizationId = OrganizationId (ctx.Arg ("organizationId")) + let chatRoomId = ChatRoomId (ctx.Arg ("chatRoomId")) + let memberPrivId = MemberPrivateId (ctx.Arg ("memberId")) + let messageId = MessageId (ctx.Arg ("messageId")) + + memberPrivId + |> authenticateMemberInOrganization organizationId + |> Result.bind (fun (organization, theMember) -> + chatRoomId + |> validateChatRoomExistence organization + |> Result.bind (fun chatRoom -> + messageId + |> validateMessageExistence chatRoom + |> Result.map (fun x -> (chatRoom, x))) + |> Result.map (fun (chatRoom, chatMessage) -> (organization, chatRoom, theMember, chatMessage))) + |> Result.map (fun (_, chatRoom, theMember, chatMessage) -> + FakePersistence.ChatRoomMessages <- + FakePersistence.ChatRoomMessages + |> Map.remove (chatRoom.Id, chatMessage.Id) + + chatRoom.Id + |> publishChatRoomEvent (DeletedMessage chatMessage.Id) + + true) + |> succeedOrRaiseGraphQLEx ) - |> Result.map - (fun (_, chatRoom, theMember, chatMessage) -> - FakePersistence.ChatRoomMessages <- - FakePersistence.ChatRoomMessages - |> Map.remove (chatRoom.Id, chatMessage.Id) + ] + ) - chatRoom.Id - |> publishChatRoomEvent (DeletedMessage chatMessage.Id) + let rootDef = + Define.Object ( + name = "Root", + description = "contains general request information", + isTypeOf = (fun o -> o :? Root), + fieldsFn = + fun () -> [ + Define.Field ("requestId", StringType, "The request's unique ID.", (fun _ (r : Root) -> r.RequestId)) + ] + ) - true + let subscription = + Define.SubscriptionObject ( + name = "Subscription", + fields = [ + Define.SubscriptionField ( + chatRoomEvents_subscription_name, + rootDef, + chatRoomEventDef, + "events related to a specific chat room", + [ + Define.Input ("chatRoomId", GuidType, description = "the ID of the chat room to listen to events from") + Define.Input ("memberId", GuidType, description = "the member's private ID") + ], + (fun ctx _ (chatRoomEvent : ChatRoomEvent) -> + let chatRoomIdOfInterest = ChatRoomId (ctx.Arg ("chatRoomId")) + let memberId = MemberPrivateId (ctx.Arg ("memberId")) + + if chatRoomEvent.ChatRoomId <> chatRoomIdOfInterest then + None + else + let chatRoom = + FakePersistence.ChatRooms + |> Map.find chatRoomEvent.ChatRoomId + let chatRoomMembersPrivIds = + FakePersistence.Members.Values + |> Seq.filter (fun m -> chatRoom.Members |> List.contains m.Id) + |> Seq.map (fun m -> m.PrivId) + if not (chatRoomMembersPrivIds |> Seq.contains memberId) then + None + else + Some chatRoomEvent) ) - |> succeedOrRaiseGraphQLEx - ) - ] - ) - - let rootDef = - Define.Object( - name = "Root", - description = "contains general request information", - isTypeOf = (fun o -> o :? Root), - fieldsFn = fun () -> - [ Define.Field("requestId", StringType, "The request's unique ID.", fun _ (r : Root) -> r.RequestId) ] - ) - - let subscription = - Define.SubscriptionObject( - name = "Subscription", - fields = [ - Define.SubscriptionField( - chatRoomEvents_subscription_name, - rootDef, - chatRoomEventDef, - "events related to a specific chat room", - [ Define.Input("chatRoomId", GuidType, description = "the ID of the chat room to listen to events from") - Define.Input("memberId", GuidType, description = "the member's private ID")], - (fun ctx _ (chatRoomEvent : ChatRoomEvent) -> - let chatRoomIdOfInterest = ChatRoomId (ctx.Arg("chatRoomId")) - let memberId = MemberPrivateId (ctx.Arg("memberId")) - - if chatRoomEvent.ChatRoomId <> chatRoomIdOfInterest then - None - else - let chatRoom = FakePersistence.ChatRooms |> Map.find chatRoomEvent.ChatRoomId - let chatRoomMembersPrivIds = - FakePersistence.Members.Values - |> Seq.filter (fun m -> chatRoom.Members |> List.contains m.Id) - |> Seq.map (fun m -> m.PrivId) - if not (chatRoomMembersPrivIds |> Seq.contains memberId) then - None - else - Some chatRoomEvent - ) + ] ) - ] - ) - let schema : ISchema = Schema(query, mutation, subscription, schemaConfig) + let schema : ISchema = Schema (query, mutation, subscription, schemaConfig) - let executor = Executor(schema, []) \ No newline at end of file + let executor = Executor (schema, []) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs index 1d1cef7c2..f10d837e1 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs @@ -1,4 +1,4 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore type InvalidMessageException (explanation : string) = - inherit System.Exception(explanation) \ No newline at end of file + inherit System.Exception (explanation) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 307264950..63a253a64 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -19,158 +19,137 @@ type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult module HttpHandlers = open System.Collections.Immutable - let private httpOk (cancellationToken : CancellationToken) (customHandler : HttpHandler) (serializerOptions : JsonSerializerOptions) payload : HttpHandler = + let private httpOk + (cancellationToken : CancellationToken) + (customHandler : HttpHandler) + (serializerOptions : JsonSerializerOptions) + payload + : HttpHandler = setStatusCode 200 >=> customHandler >=> (setHttpHeader "Content-Type" "application/json") >=> (fun _ ctx -> - JsonSerializer - .SerializeAsync( - ctx.Response.Body, - payload, - options = serializerOptions, - cancellationToken = cancellationToken - ) - .ContinueWith(fun _ -> Some ctx) // what about when serialization fails? Maybe it will never at this stage anyway... - ) + JsonSerializer + .SerializeAsync(ctx.Response.Body, payload, options = serializerOptions, cancellationToken = cancellationToken) + .ContinueWith (fun _ -> Some ctx) // what about when serialization fails? Maybe it will never at this stage anyway... + ) let private prepareGenericErrors (errorMessages : string list) = - (NameValueLookup.ofList - [ "errors", - upcast - ( errorMessages - |> List.map - (fun msg -> - NameValueLookup.ofList ["message", upcast msg] - ) - ) - ] - ) + (NameValueLookup.ofList [ + "errors", + upcast + (errorMessages + |> List.map (fun msg -> NameValueLookup.ofList [ "message", upcast msg ])) + ]) /// HttpHandler for handling GraphQL requests with Giraffe. /// This one is for specifying an interceptor when you need to /// do a custom handling on the response. For example, when you want /// to add custom headers. let handleGraphQLWithResponseInterception<'Root> - (cancellationToken : CancellationToken) - (logger : ILogger) - (interceptor : HttpHandler) - (next : HttpFunc) (ctx : HttpContext) = - task { - let cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ctx.RequestAborted).Token - if cancellationToken.IsCancellationRequested then - return (fun _ -> None) ctx - else - let options = ctx.RequestServices.GetRequiredService>() - let executor = options.SchemaExecutor - let rootFactory = options.RootFactory - let serializerOptions = options.SerializerOptions - let deserializeGraphQLRequest () = - task { - try - let! deserialized = - JsonSerializer.DeserializeAsync( - ctx.Request.Body, - serializerOptions - ) - return Ok deserialized - with - | :? GraphQLException as ex -> - logger.LogError(``exception`` = ex, message = "Error while deserializing request.") - return Result.Error [$"%s{ex.Message}\n%s{ex.ToString()}"] - } + (cancellationToken : CancellationToken) + (logger : ILogger) + (interceptor : HttpHandler) + (next : HttpFunc) + (ctx : HttpContext) + = + task { + let cancellationToken = + CancellationTokenSource + .CreateLinkedTokenSource(cancellationToken, ctx.RequestAborted) + .Token + if cancellationToken.IsCancellationRequested then + return (fun _ -> None) ctx + else + let options = ctx.RequestServices.GetRequiredService> () + let executor = options.SchemaExecutor + let rootFactory = options.RootFactory + let serializerOptions = options.SerializerOptions + let deserializeGraphQLRequest () = task { + try + let! deserialized = JsonSerializer.DeserializeAsync (ctx.Request.Body, serializerOptions) + return Ok deserialized + with :? GraphQLException as ex -> + logger.LogError (``exception`` = ex, message = "Error while deserializing request.") + return Result.Error [ $"%s{ex.Message}\n%s{ex.ToString ()}" ] + } - let applyPlanExecutionResult (result : GQLExecutionResult) = - task { + let applyPlanExecutionResult (result : GQLExecutionResult) = task { let gqlResponse = - match result.Content with - | Direct (data, errs) -> - GQLResponse.Direct(result.DocumentId, data, errs) - | RequestError (problemDetailsList) -> - GQLResponse.RequestError( - result.DocumentId, - problemDetailsList - ) - | _ -> - GQLResponse.RequestError( - result.DocumentId, - [ GQLProblemDetails.Create( - "subscriptions are not supported here (use the websocket endpoint instead)." - )] - ) + match result.Content with + | Direct (data, errs) -> GQLResponse.Direct (result.DocumentId, data, errs) + | RequestError (problemDetailsList) -> GQLResponse.RequestError (result.DocumentId, problemDetailsList) + | _ -> + GQLResponse.RequestError ( + result.DocumentId, + [ + GQLProblemDetails.Create ("subscriptions are not supported here (use the websocket endpoint instead).") + ] + ) return! httpOk cancellationToken interceptor serializerOptions gqlResponse next ctx } - let handleDeserializedGraphQLRequest (graphqlRequest : GraphQLRequest) = - task { + let handleDeserializedGraphQLRequest (graphqlRequest : GraphQLRequest) = task { match graphqlRequest.Query with - | None -> - let! result = executor.AsyncExecute (IntrospectionQuery.Definition) |> Async.StartAsTask - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug($"Result metadata: %A{result.Metadata}") + | None -> + let! result = + executor.AsyncExecute (IntrospectionQuery.Definition) + |> Async.StartAsTask + if logger.IsEnabled (LogLevel.Debug) then + logger.LogDebug ($"Result metadata: %A{result.Metadata}") + else + () + return! result |> applyPlanExecutionResult + | Some queryAsStr -> + let graphQLQueryDecodingResult = + queryAsStr + |> GraphQLQueryDecoding.decodeGraphQLQuery + serializerOptions + executor + graphqlRequest.OperationName + graphqlRequest.Variables + match graphQLQueryDecodingResult with + | Result.Error struct (docId, probDetails) -> + return! httpOk cancellationToken interceptor serializerOptions (GQLResponse.RequestError (docId, probDetails)) next ctx + | Ok query -> + if logger.IsEnabled (LogLevel.Debug) then + logger.LogDebug ($"Received query: %A{query}") + else + () + let root = rootFactory (ctx) + let! result = + let variables = + ImmutableDictionary.CreateRange ( + query.Variables + |> Map.map (fun _ value -> JsonSerializer.SerializeToElement (value)) + ) + executor.AsyncExecute (query.ExecutionPlan, data = root, variables = variables) + |> Async.StartAsTask + if logger.IsEnabled (LogLevel.Debug) then + logger.LogDebug ($"Result metadata: %A{result.Metadata}") else () return! result |> applyPlanExecutionResult - | Some queryAsStr -> - let graphQLQueryDecodingResult = - queryAsStr - |> GraphQLQueryDecoding.decodeGraphQLQuery - serializerOptions - executor - graphqlRequest.OperationName - graphqlRequest.Variables - match graphQLQueryDecodingResult with - | Result.Error struct (docId, probDetails) -> - return! - httpOk cancellationToken interceptor serializerOptions (GQLResponse.RequestError (docId, probDetails)) next ctx - | Ok query -> - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug($"Received query: %A{query}") - else - () - let root = rootFactory(ctx) - let! result = - let variables = ImmutableDictionary.CreateRange( - query.Variables - |> Map.map (fun _ value -> JsonSerializer.SerializeToElement(value)) - ) - executor.AsyncExecute( - query.ExecutionPlan, - data = root, - variables = variables - )|> Async.StartAsTask - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug($"Result metadata: %A{result.Metadata}") - else - () - return! result |> applyPlanExecutionResult - } - if ctx.Request.Headers.ContentLength.GetValueOrDefault(0) = 0 then - let! result = executor.AsyncExecute (IntrospectionQuery.Definition) |> Async.StartAsTask - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug($"Result metadata: %A{result.Metadata}") + } + if ctx.Request.Headers.ContentLength.GetValueOrDefault (0) = 0 then + let! result = + executor.AsyncExecute (IntrospectionQuery.Definition) + |> Async.StartAsTask + if logger.IsEnabled (LogLevel.Debug) then + logger.LogDebug ($"Result metadata: %A{result.Metadata}") + else + () + return! result |> applyPlanExecutionResult else - () - return! result |> applyPlanExecutionResult - else - match! deserializeGraphQLRequest() with - | Result.Error errMsgs -> - let probDetails = - errMsgs - |> List.map (fun msg -> GQLProblemDetails.Create(msg, Skip)) - return! httpOk cancellationToken interceptor serializerOptions (GQLResponse.RequestError(-1, probDetails)) next ctx - | Ok graphqlRequest -> - return! handleDeserializedGraphQLRequest graphqlRequest + match! deserializeGraphQLRequest () with + | Result.Error errMsgs -> + let probDetails = + errMsgs + |> List.map (fun msg -> GQLProblemDetails.Create (msg, Skip)) + return! httpOk cancellationToken interceptor serializerOptions (GQLResponse.RequestError (-1, probDetails)) next ctx + | Ok graphqlRequest -> return! handleDeserializedGraphQLRequest graphqlRequest } /// HttpHandler for handling GraphQL requests with Giraffe - let handleGraphQL<'Root> - (cancellationToken : CancellationToken) - (logger : ILogger) - (next : HttpFunc) (ctx : HttpContext) = - handleGraphQLWithResponseInterception<'Root> - cancellationToken - logger - id - next - ctx + let handleGraphQL<'Root> (cancellationToken : CancellationToken) (logger : ILogger) (next : HttpFunc) (ctx : HttpContext) = + handleGraphQLWithResponseInterception<'Root> cancellationToken logger id next ctx diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs index 5468d8a2b..11459e4d0 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs @@ -6,17 +6,17 @@ open System.Text.Json open System.Threading.Tasks open Microsoft.AspNetCore.Http -type PingHandler = - IServiceProvider -> JsonDocument option -> Task +type PingHandler = IServiceProvider -> JsonDocument option -> Task -type GraphQLTransportWSOptions = - { EndpointUrl: string - ConnectionInitTimeoutInMs: int - CustomPingHandler : PingHandler option } +type GraphQLTransportWSOptions = { + EndpointUrl : string + ConnectionInitTimeoutInMs : int + CustomPingHandler : PingHandler option +} -type GraphQLOptions<'Root> = - { SchemaExecutor: Executor<'Root> - RootFactory: HttpContext -> 'Root - SerializerOptions: JsonSerializerOptions - WebsocketOptions: GraphQLTransportWSOptions - } \ No newline at end of file +type GraphQLOptions<'Root> = { + SchemaExecutor : Executor<'Root> + RootFactory : HttpContext -> 'Root + SerializerOptions : JsonSerializerOptions + WebsocketOptions : GraphQLTransportWSOptions +} diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs index 70734023c..3ebd2246d 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs @@ -1,31 +1,29 @@ module internal FSharp.Data.GraphQL.Server.AspNetCore.GraphQLSubscriptionsManagement -let addSubscription (id : SubscriptionId, unsubscriber : SubscriptionUnsubscriber, onUnsubscribe : OnUnsubscribeAction) - (subscriptions : SubscriptionsDict) = - subscriptions.Add(id, (unsubscriber, onUnsubscribe)) +let addSubscription + (id : SubscriptionId, unsubscriber : SubscriptionUnsubscriber, onUnsubscribe : OnUnsubscribeAction) + (subscriptions : SubscriptionsDict) + = + subscriptions.Add (id, (unsubscriber, onUnsubscribe)) -let isIdTaken (id : SubscriptionId) (subscriptions : SubscriptionsDict) = - subscriptions.ContainsKey(id) +let isIdTaken (id : SubscriptionId) (subscriptions : SubscriptionsDict) = subscriptions.ContainsKey (id) let executeOnUnsubscribeAndDispose (id : SubscriptionId) (subscription : SubscriptionUnsubscriber * OnUnsubscribeAction) = match subscription with | unsubscriber, onUnsubscribe -> - try - id |> onUnsubscribe - finally - unsubscriber.Dispose() + try + id |> onUnsubscribe + finally + unsubscriber.Dispose () -let removeSubscription (id: SubscriptionId) (subscriptions : SubscriptionsDict) = - if subscriptions.ContainsKey(id) then - subscriptions.[id] - |> executeOnUnsubscribeAndDispose id - subscriptions.Remove(id) |> ignore +let removeSubscription (id : SubscriptionId) (subscriptions : SubscriptionsDict) = + if subscriptions.ContainsKey (id) then + subscriptions.[id] |> executeOnUnsubscribeAndDispose id + subscriptions.Remove (id) |> ignore let removeAllSubscriptions (subscriptions : SubscriptionsDict) = - subscriptions - |> Seq.iter - (fun subscription -> - subscription.Value - |> executeOnUnsubscribeAndDispose subscription.Key - ) - subscriptions.Clear() \ No newline at end of file + subscriptions + |> Seq.iter (fun subscription -> + subscription.Value + |> executeOnUnsubscribeAndDispose subscription.Key) + subscriptions.Clear () diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 084cd35c1..fc16491d1 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -14,384 +14,367 @@ open Microsoft.Extensions.Hosting open Microsoft.Extensions.Logging open System.Collections.Immutable -type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifetime : IHostApplicationLifetime, serviceProvider : IServiceProvider, logger : ILogger>, options : GraphQLOptions<'Root>) = +type GraphQLWebSocketMiddleware<'Root> + ( + next : RequestDelegate, + applicationLifetime : IHostApplicationLifetime, + serviceProvider : IServiceProvider, + logger : ILogger>, + options : GraphQLOptions<'Root> + ) = - let serializeServerMessage (jsonSerializerOptions: JsonSerializerOptions) (serverMessage : ServerMessage) = - task { + let serializeServerMessage (jsonSerializerOptions : JsonSerializerOptions) (serverMessage : ServerMessage) = task { let raw = - match serverMessage with - | ConnectionAck -> - { Id = None - Type = "connection_ack" - Payload = None } - | ServerPing -> - { Id = None - Type = "ping" - Payload = None } - | ServerPong p -> - { Id = None - Type = "pong" - Payload = p |> Option.map CustomResponse } - | Next (id, payload) -> - { Id = Some id - Type = "next" - Payload = Some <| ExecutionResult payload } - | Complete id -> - { Id = Some id - Type = "complete" - Payload = None } - | Error (id, errMsgs) -> - { Id = Some id - Type = "error" - Payload = Some <| ErrorMessages errMsgs } - return JsonSerializer.Serialize(raw, jsonSerializerOptions) - } + match serverMessage with + | ConnectionAck -> { Id = None; Type = "connection_ack"; Payload = None } + | ServerPing -> { Id = None; Type = "ping"; Payload = None } + | ServerPong p -> { Id = None; Type = "pong"; Payload = p |> Option.map CustomResponse } + | Next (id, payload) -> { Id = Some id; Type = "next"; Payload = Some <| ExecutionResult payload } + | Complete id -> { Id = Some id; Type = "complete"; Payload = None } + | Error (id, errMsgs) -> { Id = Some id; Type = "error"; Payload = Some <| ErrorMessages errMsgs } + return JsonSerializer.Serialize (raw, jsonSerializerOptions) + } - let deserializeClientMessage (serializerOptions : JsonSerializerOptions) (msg: string) = - taskResult { + let deserializeClientMessage (serializerOptions : JsonSerializerOptions) (msg : string) = taskResult { try - return JsonSerializer.Deserialize(msg, serializerOptions) + return JsonSerializer.Deserialize (msg, serializerOptions) with - | :? InvalidMessageException as e -> - return! Result.Error <| - InvalidMessage(4400, e.Message.ToString()) + | :? InvalidMessageException as e -> return! Result.Error <| InvalidMessage (4400, e.Message.ToString ()) | :? JsonException as e -> - if logger.IsEnabled(LogLevel.Debug) then - logger.LogDebug(e.ToString()) - else - () - return! Result.Error <| - InvalidMessage(4400, "invalid json in client message") + if logger.IsEnabled (LogLevel.Debug) then + logger.LogDebug (e.ToString ()) + else + () + return! + Result.Error + <| InvalidMessage (4400, "invalid json in client message") } - let isSocketOpen (theSocket : WebSocket) = - not (theSocket.State = WebSocketState.Aborted) && - not (theSocket.State = WebSocketState.Closed) && - not (theSocket.State = WebSocketState.CloseReceived) + let isSocketOpen (theSocket : WebSocket) = + not (theSocket.State = WebSocketState.Aborted) + && not (theSocket.State = WebSocketState.Closed) + && not (theSocket.State = WebSocketState.CloseReceived) - let canCloseSocket (theSocket : WebSocket) = - not (theSocket.State = WebSocketState.Aborted) && - not (theSocket.State = WebSocketState.Closed) + let canCloseSocket (theSocket : WebSocket) = + not (theSocket.State = WebSocketState.Aborted) + && not (theSocket.State = WebSocketState.Closed) - let receiveMessageViaSocket (cancellationToken : CancellationToken) (serializerOptions: JsonSerializerOptions) (socket : WebSocket) = - taskResult { - let buffer = Array.zeroCreate 4096 - let completeMessage = new List() - let mutable segmentResponse : WebSocketReceiveResult = null - while (not cancellationToken.IsCancellationRequested) && - socket |> isSocketOpen && - ((segmentResponse = null) || (not segmentResponse.EndOfMessage)) do - try - let! r = socket.ReceiveAsync(new ArraySegment(buffer), cancellationToken) - segmentResponse <- r - completeMessage.AddRange(new ArraySegment(buffer, 0, r.Count)) - with :? OperationCanceledException -> - () + let receiveMessageViaSocket (cancellationToken : CancellationToken) (serializerOptions : JsonSerializerOptions) (socket : WebSocket) = taskResult { + let buffer = Array.zeroCreate 4096 + let completeMessage = new List () + let mutable segmentResponse : WebSocketReceiveResult = null + while (not cancellationToken.IsCancellationRequested) + && socket |> isSocketOpen + && ((segmentResponse = null) + || (not segmentResponse.EndOfMessage)) do + try + let! r = socket.ReceiveAsync (new ArraySegment (buffer), cancellationToken) + segmentResponse <- r + completeMessage.AddRange (new ArraySegment (buffer, 0, r.Count)) + with :? OperationCanceledException -> + () - // TODO: Allocate string only if a debugger is attached - let message = - completeMessage - |> Seq.filter (fun x -> x > 0uy) - |> Array.ofSeq - |> System.Text.Encoding.UTF8.GetString - if String.IsNullOrWhiteSpace message then - return None - else - let! result = - message - |> deserializeClientMessage serializerOptions - return Some result + // TODO: Allocate string only if a debugger is attached + let message = + completeMessage + |> Seq.filter (fun x -> x > 0uy) + |> Array.ofSeq + |> System.Text.Encoding.UTF8.GetString + if String.IsNullOrWhiteSpace message then + return None + else + let! result = message |> deserializeClientMessage serializerOptions + return Some result } - let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) = - task { - if not (socket.State = WebSocketState.Open) then - logger.LogTrace("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) - else - // TODO: Allocate string only if a debugger is attached - let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions - let segment = - new ArraySegment( - System.Text.Encoding.UTF8.GetBytes(serializedMessage) - ) + let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) = task { if not (socket.State = WebSocketState.Open) then - logger.LogTrace("ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) + logger.LogTrace ("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) else - do! socket.SendAsync(segment, WebSocketMessageType.Text, endOfMessage = true, cancellationToken = CancellationToken.None) + // TODO: Allocate string only if a debugger is attached + let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions + let segment = new ArraySegment (System.Text.Encoding.UTF8.GetBytes (serializedMessage)) + if not (socket.State = WebSocketState.Open) then + logger.LogTrace ("ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) + else + do! socket.SendAsync (segment, WebSocketMessageType.Text, endOfMessage = true, cancellationToken = CancellationToken.None) - logger.LogTrace("<- Response: {response}", message) + logger.LogTrace ("<- Response: {response}", message) } - let addClientSubscription - (id : SubscriptionId) - (howToSendDataOnNext: SubscriptionId -> 'ResponseContent -> Task) - ( subscriptions : SubscriptionsDict, - socket : WebSocket, - streamSource: IObservable<'ResponseContent>, - jsonSerializerOptions : JsonSerializerOptions ) = - let observer = new Reactive.AnonymousObserver<'ResponseContent>( - onNext = - (fun theOutput -> - theOutput - |> howToSendDataOnNext id - |> Task.WaitAll - ), - onError = - (fun ex -> - logger.LogError(ex, "Error on subscription with id='{id}'", id) - ), - onCompleted = - (fun () -> - Complete id - |> sendMessageViaSocket jsonSerializerOptions (socket) - |> Async.AwaitTask - |> Async.RunSynchronously - subscriptions - |> GraphQLSubscriptionsManagement.removeSubscription(id) - ) - ) + let addClientSubscription + (id : SubscriptionId) + (howToSendDataOnNext : SubscriptionId -> 'ResponseContent -> Task) + (subscriptions : SubscriptionsDict, + socket : WebSocket, + streamSource : IObservable<'ResponseContent>, + jsonSerializerOptions : JsonSerializerOptions) + = + let observer = + new Reactive.AnonymousObserver<'ResponseContent> ( + onNext = (fun theOutput -> theOutput |> howToSendDataOnNext id |> Task.WaitAll), + onError = (fun ex -> logger.LogError (ex, "Error on subscription with id='{id}'", id)), + onCompleted = + (fun () -> + Complete id + |> sendMessageViaSocket jsonSerializerOptions (socket) + |> Async.AwaitTask + |> Async.RunSynchronously + subscriptions + |> GraphQLSubscriptionsManagement.removeSubscription (id)) + ) - let unsubscriber = streamSource.Subscribe(observer) + let unsubscriber = streamSource.Subscribe (observer) - subscriptions - |> GraphQLSubscriptionsManagement.addSubscription(id, unsubscriber, (fun _ -> ())) + subscriptions + |> GraphQLSubscriptionsManagement.addSubscription (id, unsubscriber, (fun _ -> ())) - let tryToGracefullyCloseSocket (code, message) theSocket = - if theSocket |> canCloseSocket - then - theSocket.CloseAsync(code, message, CancellationToken.None) - else - Task.CompletedTask + let tryToGracefullyCloseSocket (code, message) theSocket = + if theSocket |> canCloseSocket then + theSocket.CloseAsync (code, message, CancellationToken.None) + else + Task.CompletedTask - let tryToGracefullyCloseSocketWithDefaultBehavior = - tryToGracefullyCloseSocket (WebSocketCloseStatus.NormalClosure, "Normal Closure") + let tryToGracefullyCloseSocketWithDefaultBehavior = + tryToGracefullyCloseSocket (WebSocketCloseStatus.NormalClosure, "Normal Closure") - let handleMessages (cancellationToken: CancellationToken) (httpContext: HttpContext) (serializerOptions: JsonSerializerOptions) (executor : Executor<'Root>) (root: HttpContext -> 'Root) (pingHandler : PingHandler option) (socket : WebSocket) = - let subscriptions = new Dictionary() - // ----------> - // Helpers --> - // ----------> - let rcvMsgViaSocket = receiveMessageViaSocket (CancellationToken.None) + let handleMessages + (cancellationToken : CancellationToken) + (httpContext : HttpContext) + (serializerOptions : JsonSerializerOptions) + (executor : Executor<'Root>) + (root : HttpContext -> 'Root) + (pingHandler : PingHandler option) + (socket : WebSocket) + = + let subscriptions = new Dictionary () + // ----------> + // Helpers --> + // ----------> + let rcvMsgViaSocket = receiveMessageViaSocket (CancellationToken.None) - let sendMsg = sendMessageViaSocket serializerOptions socket - let rcv() = - socket - |> rcvMsgViaSocket serializerOptions + let sendMsg = sendMessageViaSocket serializerOptions socket + let rcv () = socket |> rcvMsgViaSocket serializerOptions - let sendOutput id (output : Output) = - match output.TryGetValue("errors") with - | true, theValue -> - // The specification says: "This message terminates the operation and no further messages will be sent." - subscriptions - |> GraphQLSubscriptionsManagement.removeSubscription(id) - sendMsg (Error (id, unbox theValue)) - | false, _ -> - sendMsg (Next (id, output)) + let sendOutput id (output : Output) = + match output.TryGetValue ("errors") with + | true, theValue -> + // The specification says: "This message terminates the operation and no further messages will be sent." + subscriptions + |> GraphQLSubscriptionsManagement.removeSubscription (id) + sendMsg (Error (id, unbox theValue)) + | false, _ -> sendMsg (Next (id, output)) - let sendSubscriptionResponseOutput id subscriptionResult = - match subscriptionResult with - | SubscriptionResult output -> - output - |> sendOutput id - | SubscriptionErrors (output, errors) -> - printfn "Subscription errors: %s" (String.Join('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) - Task.FromResult(()) + let sendSubscriptionResponseOutput id subscriptionResult = + match subscriptionResult with + | SubscriptionResult output -> output |> sendOutput id + | SubscriptionErrors (output, errors) -> + printfn "Subscription errors: %s" (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) + Task.FromResult (()) - let sendDeferredResponseOutput id deferredResult = - match deferredResult with - | DeferredResult (obj, path) -> - let output = obj :?> Dictionary - output - |> sendOutput id - | DeferredErrors (obj, errors, _) -> - printfn "Deferred response errors: %s" (String.Join('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) - Task.FromResult(()) + let sendDeferredResponseOutput id deferredResult = + match deferredResult with + | DeferredResult (obj, path) -> + let output = obj :?> Dictionary + output |> sendOutput id + | DeferredErrors (obj, errors, _) -> + printfn "Deferred response errors: %s" (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) + Task.FromResult (()) - let sendDeferredResultDelayedBy (cancToken: CancellationToken) (ms: int) id deferredResult = - task { - do! Async.StartAsTask(Async.Sleep ms, cancellationToken = cancToken) - do! deferredResult - |> sendDeferredResponseOutput id + let sendDeferredResultDelayedBy (cancToken : CancellationToken) (ms : int) id deferredResult = task { + do! Async.StartAsTask (Async.Sleep ms, cancellationToken = cancToken) + do! deferredResult |> sendDeferredResponseOutput id } - let sendQueryOutputDelayedBy = sendDeferredResultDelayedBy cancellationToken + let sendQueryOutputDelayedBy = sendDeferredResultDelayedBy cancellationToken - let applyPlanExecutionResult (id: SubscriptionId) (socket) (executionResult: GQLExecutionResult) = - task { + let applyPlanExecutionResult (id : SubscriptionId) (socket) (executionResult : GQLExecutionResult) = task { match executionResult with | Stream observableOutput -> (subscriptions, socket, observableOutput, serializerOptions) |> addClientSubscription id sendSubscriptionResponseOutput | Deferred (data, errors, observableOutput) -> - do! data - |> sendOutput id + do! data |> sendOutput id if errors.IsEmpty then - (subscriptions, socket, observableOutput, serializerOptions) - |> addClientSubscription id (sendQueryOutputDelayedBy 5000) + (subscriptions, socket, observableOutput, serializerOptions) + |> addClientSubscription id (sendQueryOutputDelayedBy 5000) else - () - | Direct (data, _) -> - do! data - |> sendOutput id - | RequestError problemDetails -> - printfn "Request error: %s" (String.Join('\n', problemDetails |> Seq.map (fun x -> $"- %s{x.Message}"))) + () + | Direct (data, _) -> do! data |> sendOutput id + | RequestError problemDetails -> printfn "Request error: %s" (String.Join ('\n', problemDetails |> Seq.map (fun x -> $"- %s{x.Message}"))) } - let getStrAddendumOfOptionalPayload optionalPayload = - optionalPayload - |> Option.map (fun payloadStr -> $" with payload: %A{payloadStr}") - |> Option.defaultWith (fun () -> "") + let getStrAddendumOfOptionalPayload optionalPayload = + optionalPayload + |> Option.map (fun payloadStr -> $" with payload: %A{payloadStr}") + |> Option.defaultWith (fun () -> "") - let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) = - logger.LogTrace ("{message}{messageaddendum}", msgAsStr, (optionalPayload |> getStrAddendumOfOptionalPayload)) + let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) = + logger.LogTrace ("{message}{messageaddendum}", msgAsStr, (optionalPayload |> getStrAddendumOfOptionalPayload)) - let logMsgWithIdReceived (id : string) (msgAsStr : string) = - logger.LogTrace("{message} (id: {messageid})", msgAsStr, id) + let logMsgWithIdReceived (id : string) (msgAsStr : string) = logger.LogTrace ("{message} (id: {messageid})", msgAsStr, id) - // <-------------- - // <-- Helpers --| - // <-------------- + // <-------------- + // <-- Helpers --| + // <-------------- - // -------> - // Main --> - // -------> - task { - try - while not cancellationToken.IsCancellationRequested && socket |> isSocketOpen do - let! receivedMessage = rcv() - match receivedMessage with - | Result.Error failureMsgs -> - "InvalidMessage" |> logMsgReceivedWithOptionalPayload None - match failureMsgs with - | InvalidMessage (code, explanation) -> - do! socket.CloseAsync(enum code, explanation, CancellationToken.None) - | Ok maybeMsg -> - match maybeMsg with - | None -> - logger.LogTrace("Websocket socket received empty message! (socket state = {socketstate})", socket.State) - | Some msg -> - match msg with - | ConnectionInit p -> - "ConnectionInit" |> logMsgReceivedWithOptionalPayload p - do! socket.CloseAsync( - enum CustomWebSocketStatus.tooManyInitializationRequests, - "too many initialization requests", - CancellationToken.None) - | ClientPing p -> - "ClientPing" |> logMsgReceivedWithOptionalPayload p - match pingHandler with - | Some func -> - let! customP = p |> func serviceProvider - do! ServerPong customP |> sendMsg - | None -> - do! ServerPong p |> sendMsg - | ClientPong p -> - "ClientPong" |> logMsgReceivedWithOptionalPayload p - | Subscribe (id, query) -> - "Subscribe" |> logMsgWithIdReceived id - if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then - do! socket.CloseAsync( - enum CustomWebSocketStatus.subscriberAlreadyExists, - $"Subscriber for %s{id} already exists", - CancellationToken.None) - else - let variables = ImmutableDictionary.CreateRange(query.Variables |> Map.map (fun _ value -> value :?> JsonElement)) - let! planExecutionResult = - executor.AsyncExecute(query.ExecutionPlan, root(httpContext), variables) - |> Async.StartAsTask - do! planExecutionResult - |> applyPlanExecutionResult id socket - | ClientComplete id -> - "ClientComplete" |> logMsgWithIdReceived id - subscriptions |> GraphQLSubscriptionsManagement.removeSubscription (id) - logger.LogTrace "Leaving graphql-ws connection loop..." - do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior - with - | ex -> - logger.LogError(ex, "Cannot handle a message; dropping a websocket connection") - // at this point, only something really weird must have happened. - // In order to avoid faulty state scenarios and unimagined damages, - // just close the socket without further ado. - do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior - } + // -------> + // Main --> + // -------> + task { + try + while not cancellationToken.IsCancellationRequested + && socket |> isSocketOpen do + let! receivedMessage = rcv () + match receivedMessage with + | Result.Error failureMsgs -> + "InvalidMessage" |> logMsgReceivedWithOptionalPayload None + match failureMsgs with + | InvalidMessage (code, explanation) -> do! socket.CloseAsync (enum code, explanation, CancellationToken.None) + | Ok maybeMsg -> + match maybeMsg with + | None -> logger.LogTrace ("Websocket socket received empty message! (socket state = {socketstate})", socket.State) + | Some msg -> + match msg with + | ConnectionInit p -> + "ConnectionInit" |> logMsgReceivedWithOptionalPayload p + do! + socket.CloseAsync ( + enum CustomWebSocketStatus.tooManyInitializationRequests, + "too many initialization requests", + CancellationToken.None + ) + | ClientPing p -> + "ClientPing" |> logMsgReceivedWithOptionalPayload p + match pingHandler with + | Some func -> + let! customP = p |> func serviceProvider + do! ServerPong customP |> sendMsg + | None -> do! ServerPong p |> sendMsg + | ClientPong p -> "ClientPong" |> logMsgReceivedWithOptionalPayload p + | Subscribe (id, query) -> + "Subscribe" |> logMsgWithIdReceived id + if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then + do! + socket.CloseAsync ( + enum CustomWebSocketStatus.subscriberAlreadyExists, + $"Subscriber for %s{id} already exists", + CancellationToken.None + ) + else + let variables = + ImmutableDictionary.CreateRange ( + query.Variables + |> Map.map (fun _ value -> value :?> JsonElement) + ) + let! planExecutionResult = + executor.AsyncExecute (query.ExecutionPlan, root (httpContext), variables) + |> Async.StartAsTask + do! planExecutionResult |> applyPlanExecutionResult id socket + | ClientComplete id -> + "ClientComplete" |> logMsgWithIdReceived id + subscriptions + |> GraphQLSubscriptionsManagement.removeSubscription (id) + logger.LogTrace "Leaving graphql-ws connection loop..." + do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior + with ex -> + logger.LogError (ex, "Cannot handle a message; dropping a websocket connection") + // at this point, only something really weird must have happened. + // In order to avoid faulty state scenarios and unimagined damages, + // just close the socket without further ado. + do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior + } // <-------- // <-- Main // <-------- - let waitForConnectionInitAndRespondToClient (serializerOptions : JsonSerializerOptions) (connectionInitTimeoutInMs : int) (socket : WebSocket) : TaskResult = - taskResult { - let timerTokenSource = new CancellationTokenSource() - timerTokenSource.CancelAfter(connectionInitTimeoutInMs) - let detonationRegistration = timerTokenSource.Token.Register(fun _ -> - socket - |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.connectionTimeout, "Connection initialization timeout") - |> Task.WaitAll - ) + let waitForConnectionInitAndRespondToClient + (serializerOptions : JsonSerializerOptions) + (connectionInitTimeoutInMs : int) + (socket : WebSocket) + : TaskResult = + taskResult { + let timerTokenSource = new CancellationTokenSource () + timerTokenSource.CancelAfter (connectionInitTimeoutInMs) + let detonationRegistration = + timerTokenSource.Token.Register (fun _ -> + socket + |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.connectionTimeout, "Connection initialization timeout") + |> Task.WaitAll) - let! connectionInitSucceeded = TaskResult.Run((fun _ -> - task { - logger.LogDebug("Waiting for ConnectionInit...") - let! receivedMessage = receiveMessageViaSocket (CancellationToken.None) serializerOptions socket - match receivedMessage with - | Ok (Some (ConnectionInit _)) -> - logger.LogDebug("Valid connection_init received! Responding with ACK!") - detonationRegistration.Unregister() |> ignore - do! ConnectionAck |> sendMessageViaSocket serializerOptions socket - return true - | Ok (Some (Subscribe _)) -> - do! - socket - |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.unauthorized, "Unauthorized") - return false - | Result.Error (InvalidMessage (code, explanation)) -> - do! - socket - |> tryToGracefullyCloseSocket (enum code, explanation) - return false - | _ -> - do! - socket - |> tryToGracefullyCloseSocketWithDefaultBehavior - return false - }), timerTokenSource.Token) - if (not timerTokenSource.Token.IsCancellationRequested) then - if connectionInitSucceeded then - return () - else - return! Result.Error <| "ConnectionInit failed (not because of timeout)" - else - return! Result.Error <| "ConnectionInit timeout" - } + let! connectionInitSucceeded = + TaskResult.Run ( + (fun _ -> task { + logger.LogDebug ("Waiting for ConnectionInit...") + let! receivedMessage = receiveMessageViaSocket (CancellationToken.None) serializerOptions socket + match receivedMessage with + | Ok (Some (ConnectionInit _)) -> + logger.LogDebug ("Valid connection_init received! Responding with ACK!") + detonationRegistration.Unregister () |> ignore + do! + ConnectionAck + |> sendMessageViaSocket serializerOptions socket + return true + | Ok (Some (Subscribe _)) -> + do! + socket + |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.unauthorized, "Unauthorized") + return false + | Result.Error (InvalidMessage (code, explanation)) -> + do! + socket + |> tryToGracefullyCloseSocket (enum code, explanation) + return false + | _ -> + do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior + return false + }), + timerTokenSource.Token + ) + if (not timerTokenSource.Token.IsCancellationRequested) then + if connectionInitSucceeded then + return () + else + return! + Result.Error + <| "ConnectionInit failed (not because of timeout)" + else + return! Result.Error <| "ConnectionInit timeout" + } - member __.InvokeAsync(ctx : HttpContext) = - task { - if not (ctx.Request.Path = PathString (options.WebsocketOptions.EndpointUrl)) then - do! next.Invoke(ctx) - else - if ctx.WebSockets.IsWebSocketRequest then - use! socket = ctx.WebSockets.AcceptWebSocketAsync("graphql-transport-ws") - let! connectionInitResult = - socket - |> waitForConnectionInitAndRespondToClient options.SerializerOptions options.WebsocketOptions.ConnectionInitTimeoutInMs - match connectionInitResult with - | Result.Error errMsg -> - logger.LogWarning("{warningmsg}", ($"%A{errMsg}")) - | Ok _ -> - let longRunningCancellationToken = - (CancellationTokenSource.CreateLinkedTokenSource(ctx.RequestAborted, applicationLifetime.ApplicationStopping).Token) - longRunningCancellationToken.Register(fun _ -> + member __.InvokeAsync (ctx : HttpContext) = task { + if not (ctx.Request.Path = PathString (options.WebsocketOptions.EndpointUrl)) then + do! next.Invoke (ctx) + else if ctx.WebSockets.IsWebSocketRequest then + use! socket = ctx.WebSockets.AcceptWebSocketAsync ("graphql-transport-ws") + let! connectionInitResult = socket - |> tryToGracefullyCloseSocketWithDefaultBehavior - |> Async.AwaitTask - |> Async.RunSynchronously - ) |> ignore - let safe_HandleMessages = handleMessages longRunningCancellationToken - try - do! socket - |> safe_HandleMessages ctx options.SerializerOptions options.SchemaExecutor options.RootFactory options.WebsocketOptions.CustomPingHandler - with - | ex -> - logger.LogError(ex, "Cannot handle Websocket message.") + |> waitForConnectionInitAndRespondToClient options.SerializerOptions options.WebsocketOptions.ConnectionInitTimeoutInMs + match connectionInitResult with + | Result.Error errMsg -> logger.LogWarning ("{warningmsg}", ($"%A{errMsg}")) + | Ok _ -> + let longRunningCancellationToken = + (CancellationTokenSource + .CreateLinkedTokenSource(ctx.RequestAborted, applicationLifetime.ApplicationStopping) + .Token) + longRunningCancellationToken.Register (fun _ -> + socket + |> tryToGracefullyCloseSocketWithDefaultBehavior + |> Async.AwaitTask + |> Async.RunSynchronously) + |> ignore + let safe_HandleMessages = handleMessages longRunningCancellationToken + try + do! + socket + |> safe_HandleMessages + ctx + options.SerializerOptions + options.SchemaExecutor + options.RootFactory + options.WebsocketOptions.CustomPingHandler + with ex -> + logger.LogError (ex, "Cannot handle Websocket message.") else - do! next.Invoke(ctx) - } \ No newline at end of file + do! next.Invoke (ctx) + } diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs index 149696849..12fd5fe90 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs @@ -11,40 +11,32 @@ type SubscriptionUnsubscriber = IDisposable type OnUnsubscribeAction = SubscriptionId -> unit type SubscriptionsDict = IDictionary -type GraphQLRequest = - { OperationName : string option - Query : string option - Variables : JsonDocument option - Extensions : string option } +type GraphQLRequest = { + OperationName : string option + Query : string option + Variables : JsonDocument option + Extensions : string option +} -type RawMessage = - { Id : string option - Type : string - Payload : JsonDocument option } +type RawMessage = { Id : string option; Type : string; Payload : JsonDocument option } type ServerRawPayload = | ExecutionResult of Output | ErrorMessages of NameValueLookup list | CustomResponse of JsonDocument -type RawServerMessage = - { Id : string option - Type : string - Payload : ServerRawPayload option } +type RawServerMessage = { Id : string option; Type : string; Payload : ServerRawPayload option } -type GraphQLQuery = - { ExecutionPlan : ExecutionPlan - Variables : Map } +type GraphQLQuery = { ExecutionPlan : ExecutionPlan; Variables : Map } type ClientMessage = - | ConnectionInit of payload: JsonDocument option - | ClientPing of payload: JsonDocument option - | ClientPong of payload: JsonDocument option - | Subscribe of id: string * query: GraphQLQuery - | ClientComplete of id: string + | ConnectionInit of payload : JsonDocument option + | ClientPing of payload : JsonDocument option + | ClientPong of payload : JsonDocument option + | Subscribe of id : string * query : GraphQLQuery + | ClientComplete of id : string -type ClientMessageProtocolFailure = - | InvalidMessage of code: int * explanation: string +type ClientMessageProtocolFailure = InvalidMessage of code : int * explanation : string type ServerMessage = | ConnectionAck @@ -59,4 +51,4 @@ module CustomWebSocketStatus = let unauthorized = 4401 let connectionTimeout = 4408 let subscriberAlreadyExists = 4409 - let tooManyInitializationRequests = 4429 \ No newline at end of file + let tooManyInitializationRequests = 4429 diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs index a2aa221a4..b99b39ba0 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs @@ -1,88 +1,84 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore module GraphQLQueryDecoding = - open FSharp.Data.GraphQL - open System - open System.Text.Json - open System.Text.Json.Serialization - open FsToolkit.ErrorHandling + open FSharp.Data.GraphQL + open System + open System.Text.Json + open System.Text.Json.Serialization + open FsToolkit.ErrorHandling - let genericErrorContentForDoc (docId: int) (message: string) = - struct (docId, [GQLProblemDetails.Create (message, Skip)]) + let genericErrorContentForDoc (docId : int) (message : string) = struct (docId, [ GQLProblemDetails.Create (message, Skip) ]) - let genericFinalErrorForDoc (docId: int) (message: string) = - Result.Error (genericErrorContentForDoc docId message) + let genericFinalErrorForDoc (docId : int) (message : string) = Result.Error (genericErrorContentForDoc docId message) - let genericFinalError message = - message |> genericFinalErrorForDoc -1 + let genericFinalError message = message |> genericFinalErrorForDoc -1 - let private resolveVariables (serializerOptions : JsonSerializerOptions) (expectedVariables : Types.VarDef list) (variableValuesObj : JsonDocument) = - result { - try - try - if (not (variableValuesObj.RootElement.ValueKind.Equals(JsonValueKind.Object))) then - let offendingValueKind = variableValuesObj.RootElement.ValueKind - return! Result.Error ($"\"variables\" must be an object, but here it is \"%A{offendingValueKind}\" instead") - else - let providedVariableValues = variableValuesObj.RootElement.EnumerateObject() |> List.ofSeq - return! Ok - (expectedVariables - |> List.choose - (fun expectedVariable -> - providedVariableValues - |> List.tryFind(fun x -> x.Name = expectedVariable.Name) - |> Option.map - (fun providedValue -> - let boxedValue = - if providedValue.Value.ValueKind.Equals(JsonValueKind.Null) then - null :> obj - elif providedValue.Value.ValueKind.Equals(JsonValueKind.String) then - providedValue.Value.GetString() :> obj - else - JsonSerializer.Deserialize(providedValue.Value, serializerOptions) :> obj - (expectedVariable.Name, boxedValue) - ) - ) - |> Map.ofList) - with - | :? JsonException as ex -> - return! Result.Error (ex.Message) - | :? GraphQLException as ex -> - return! Result.Error (ex.Message) - | ex -> - printfn "%s" (ex.ToString()) - return! Result.Error ("Something unexpected happened during the parsing of this request.") - finally - variableValuesObj.Dispose() - } + let private resolveVariables + (serializerOptions : JsonSerializerOptions) + (expectedVariables : Types.VarDef list) + (variableValuesObj : JsonDocument) + = + result { + try + try + if (not (variableValuesObj.RootElement.ValueKind.Equals (JsonValueKind.Object))) then + let offendingValueKind = variableValuesObj.RootElement.ValueKind + return! Result.Error ($"\"variables\" must be an object, but here it is \"%A{offendingValueKind}\" instead") + else + let providedVariableValues = + variableValuesObj.RootElement.EnumerateObject () + |> List.ofSeq + return! + Ok ( + expectedVariables + |> List.choose (fun expectedVariable -> + providedVariableValues + |> List.tryFind (fun x -> x.Name = expectedVariable.Name) + |> Option.map (fun providedValue -> + let boxedValue = + if providedValue.Value.ValueKind.Equals (JsonValueKind.Null) then + null :> obj + elif providedValue.Value.ValueKind.Equals (JsonValueKind.String) then + providedValue.Value.GetString () :> obj + else + JsonSerializer.Deserialize (providedValue.Value, serializerOptions) :> obj + (expectedVariable.Name, boxedValue))) + |> Map.ofList + ) + with + | :? JsonException as ex -> return! Result.Error (ex.Message) + | :? GraphQLException as ex -> return! Result.Error (ex.Message) + | ex -> + printfn "%s" (ex.ToString ()) + return! Result.Error ("Something unexpected happened during the parsing of this request.") + finally + variableValuesObj.Dispose () + } - let decodeGraphQLQuery (serializerOptions : JsonSerializerOptions) (executor : Executor<'a>) (operationName : string option) (variables : JsonDocument option) (query : string) = - let executionPlanResult = - result { - try - match operationName with - | Some operationName -> - return! executor.CreateExecutionPlan(query, operationName = operationName) - | None -> - return! executor.CreateExecutionPlan(query) - with - | :? JsonException as ex -> - return! genericFinalError (ex.Message) - | :? GraphQLException as ex -> - return! genericFinalError (ex.Message) - } + let decodeGraphQLQuery + (serializerOptions : JsonSerializerOptions) + (executor : Executor<'a>) + (operationName : string option) + (variables : JsonDocument option) + (query : string) + = + let executionPlanResult = result { + try + match operationName with + | Some operationName -> return! executor.CreateExecutionPlan (query, operationName = operationName) + | None -> return! executor.CreateExecutionPlan (query) + with + | :? JsonException as ex -> return! genericFinalError (ex.Message) + | :? GraphQLException as ex -> return! genericFinalError (ex.Message) + } - executionPlanResult - |> Result.bind - (fun executionPlan -> - match variables with - | None -> Ok <| (executionPlan, Map.empty) // it's none of our business here if some variables are expected. If that's the case, execution of the ExecutionPlan will take care of that later (and issue an error). - | Some variableValuesObj -> - variableValuesObj - |> resolveVariables serializerOptions executionPlan.Variables - |> Result.map (fun variableValsObj -> (executionPlan, variableValsObj)) - |> Result.mapError (genericErrorContentForDoc executionPlan.DocumentId) - ) - |> Result.map (fun (executionPlan, variables) -> - { ExecutionPlan = executionPlan - Variables = variables }) \ No newline at end of file + executionPlanResult + |> Result.bind (fun executionPlan -> + match variables with + | None -> Ok <| (executionPlan, Map.empty) // it's none of our business here if some variables are expected. If that's the case, execution of the ExecutionPlan will take care of that later (and issue an error). + | Some variableValuesObj -> + variableValuesObj + |> resolveVariables serializerOptions executionPlan.Variables + |> Result.map (fun variableValsObj -> (executionPlan, variableValsObj)) + |> Result.mapError (genericErrorContentForDoc executionPlan.DocumentId)) + |> Result.map (fun (executionPlan, variables) -> { ExecutionPlan = executionPlan; Variables = variables }) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index d58c975f7..84797060a 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -6,174 +6,166 @@ open System.Text.Json open System.Text.Json.Serialization [] -type ClientMessageConverter<'Root>(executor : Executor<'Root>) = - inherit JsonConverter() - - let raiseInvalidMsg explanation = - raise <| InvalidMessageException explanation - - /// From the spec: "Receiving a message of a type or format which is not specified in this document will result in an immediate socket closure with the event 4400: <error-message>. - /// The <error-message> can be vaguely descriptive on why the received message is invalid." - let invalidMsg (explanation : string) = - InvalidMessage (4400, explanation) - |> Result.Error - - let errMsgToStr (struct (docId: int, graphQLErrorMsgs: GQLProblemDetails list)) = - String.Join('\n', graphQLErrorMsgs |> Seq.map (fun err -> err.Message)) - - let unpackRopResult ropResult = - match ropResult with - | Ok x -> x - | Result.Error (InvalidMessage (_, explanation: string)) -> - raiseInvalidMsg explanation - - let getOptionalString (reader : byref) = - if reader.TokenType.Equals(JsonTokenType.Null) then - None - else - Some (reader.GetString()) - - let readPropertyValueAsAString (propertyName : string) (reader : byref) = - if reader.Read() then - getOptionalString(&reader) - else - raiseInvalidMsg <| $"was expecting a value for property \"%s{propertyName}\"" - - let requireId (raw : RawMessage) : Result = - match raw.Id with - | Some s -> Ok s - | None -> invalidMsg <| "property \"id\" is required for this message but was not present." - - let requireSubscribePayload (serializerOptions : JsonSerializerOptions) (executor : Executor<'a>) (payload : JsonDocument option) : Result = - match payload with - | None -> - invalidMsg <| "payload is required for this message, but none was present." - | Some p -> - let rawSubsPayload = JsonSerializer.Deserialize(p, serializerOptions) - match rawSubsPayload with - | None -> - invalidMsg <| "payload is required for this message, but none was present." - | Some subscribePayload -> - match subscribePayload.Query with +type ClientMessageConverter<'Root> (executor : Executor<'Root>) = + inherit JsonConverter () + + let raiseInvalidMsg explanation = raise <| InvalidMessageException explanation + + /// From the spec: "Receiving a message of a type or format which is not specified in this document will result in an immediate socket closure with the event 4400: <error-message>. + /// The <error-message> can be vaguely descriptive on why the received message is invalid." + let invalidMsg (explanation : string) = InvalidMessage (4400, explanation) |> Result.Error + + let errMsgToStr (struct (docId : int, graphQLErrorMsgs : GQLProblemDetails list)) = + String.Join ('\n', graphQLErrorMsgs |> Seq.map (fun err -> err.Message)) + + let unpackRopResult ropResult = + match ropResult with + | Ok x -> x + | Result.Error (InvalidMessage (_, explanation : string)) -> raiseInvalidMsg explanation + + let getOptionalString (reader : byref) = + if reader.TokenType.Equals (JsonTokenType.Null) then + None + else + Some (reader.GetString ()) + + let readPropertyValueAsAString (propertyName : string) (reader : byref) = + if reader.Read () then + getOptionalString (&reader) + else + raiseInvalidMsg + <| $"was expecting a value for property \"%s{propertyName}\"" + + let requireId (raw : RawMessage) : Result = + match raw.Id with + | Some s -> Ok s | None -> - invalidMsg <| "there was no query in the client's subscribe message!" - | Some query -> - query - |> GraphQLQueryDecoding.decodeGraphQLQuery serializerOptions executor subscribePayload.OperationName subscribePayload.Variables - |> Result.mapError (fun errMsg -> InvalidMessage (CustomWebSocketStatus.invalidMessage, errMsg |> errMsgToStr)) - - - let readRawMessage (reader : byref, options: JsonSerializerOptions) : RawMessage = - if not (reader.TokenType.Equals(JsonTokenType.StartObject)) - then raise (new JsonException($"reader's first token was not \"%A{JsonTokenType.StartObject}\", but \"%A{reader.TokenType}\"")) - else - let mutable id : string option = None - let mutable theType : string option = None - let mutable payload : JsonDocument option = None - while reader.Read() && (not (reader.TokenType.Equals(JsonTokenType.EndObject))) do - match reader.GetString() with - | "id" -> - id <- readPropertyValueAsAString "id" &reader - | "type" -> - theType <- readPropertyValueAsAString "type" &reader - | "payload" -> - payload <- Some <| JsonDocument.ParseValue(&reader) + invalidMsg + <| "property \"id\" is required for this message but was not present." + + let requireSubscribePayload + (serializerOptions : JsonSerializerOptions) + (executor : Executor<'a>) + (payload : JsonDocument option) + : Result = + match payload with + | None -> + invalidMsg + <| "payload is required for this message, but none was present." + | Some p -> + let rawSubsPayload = JsonSerializer.Deserialize (p, serializerOptions) + match rawSubsPayload with + | None -> + invalidMsg + <| "payload is required for this message, but none was present." + | Some subscribePayload -> + match subscribePayload.Query with + | None -> + invalidMsg + <| "there was no query in the client's subscribe message!" + | Some query -> + query + |> GraphQLQueryDecoding.decodeGraphQLQuery serializerOptions executor subscribePayload.OperationName subscribePayload.Variables + |> Result.mapError (fun errMsg -> InvalidMessage (CustomWebSocketStatus.invalidMessage, errMsg |> errMsgToStr)) + + + let readRawMessage (reader : byref, options : JsonSerializerOptions) : RawMessage = + if not (reader.TokenType.Equals (JsonTokenType.StartObject)) then + raise (new JsonException ($"reader's first token was not \"%A{JsonTokenType.StartObject}\", but \"%A{reader.TokenType}\"")) + else + let mutable id : string option = None + let mutable theType : string option = None + let mutable payload : JsonDocument option = None + while reader.Read () + && (not (reader.TokenType.Equals (JsonTokenType.EndObject))) do + match reader.GetString () with + | "id" -> id <- readPropertyValueAsAString "id" &reader + | "type" -> theType <- readPropertyValueAsAString "type" &reader + | "payload" -> payload <- Some <| JsonDocument.ParseValue (&reader) + | other -> raiseInvalidMsg <| $"unknown property \"%s{other}\"" + + match theType with + | None -> raiseInvalidMsg "property \"type\" is missing" + | Some msgType -> { Id = id; Type = msgType; Payload = payload } + + override __.Read (reader : byref, typeToConvert : Type, options : JsonSerializerOptions) : ClientMessage = + let raw = readRawMessage (&reader, options) + match raw.Type with + | "connection_init" -> ConnectionInit raw.Payload + | "ping" -> ClientPing raw.Payload + | "pong" -> ClientPong raw.Payload + | "complete" -> + raw + |> requireId + |> Result.map ClientComplete + |> unpackRopResult + | "subscribe" -> + raw + |> requireId + |> Result.bind (fun id -> + raw.Payload + |> requireSubscribePayload options executor + |> Result.map (fun payload -> (id, payload))) + |> Result.map Subscribe + |> unpackRopResult | other -> - raiseInvalidMsg <| $"unknown property \"%s{other}\"" - - match theType with - | None -> - raiseInvalidMsg "property \"type\" is missing" - | Some msgType -> - { Id = id - Type = msgType - Payload = payload } - - override __.Read(reader : byref, typeToConvert: Type, options: JsonSerializerOptions) : ClientMessage = - let raw = readRawMessage(&reader, options) - match raw.Type with - | "connection_init" -> - ConnectionInit raw.Payload - | "ping" -> - ClientPing raw.Payload - | "pong" -> - ClientPong raw.Payload - | "complete" -> - raw - |> requireId - |> Result.map ClientComplete - |> unpackRopResult - | "subscribe" -> - raw - |> requireId - |> Result.bind - (fun id -> - raw.Payload - |> requireSubscribePayload options executor - |> Result.map (fun payload -> (id, payload)) - ) - |> Result.map Subscribe - |> unpackRopResult - | other -> - raiseInvalidMsg <| $"invalid type \"%s{other}\" specified by client." + raiseInvalidMsg + <| $"invalid type \"%s{other}\" specified by client." - - override __.Write(writer : Utf8JsonWriter, value : ClientMessage, options : JsonSerializerOptions) = - failwith "serializing a WebSocketClientMessage is not supported (yet(?))" + override __.Write (writer : Utf8JsonWriter, value : ClientMessage, options : JsonSerializerOptions) = + failwith "serializing a WebSocketClientMessage is not supported (yet(?))" [] -type RawServerMessageConverter() = - inherit JsonConverter() - - override __.Read(reader : byref, typeToConvert: Type, options : JsonSerializerOptions) : RawServerMessage = - failwith "deserializing a RawServerMessage is not supported (yet(?))" - - override __.Write(writer : Utf8JsonWriter, value : RawServerMessage, options : JsonSerializerOptions) = - writer.WriteStartObject() - writer.WriteString("type", value.Type) - match value.Id with - | None -> - () - | Some id -> - writer.WriteString("id", id) - - match value.Payload with - | None -> - () - | Some serverRawPayload -> - match serverRawPayload with - | ExecutionResult output -> - writer.WritePropertyName("payload") - JsonSerializer.Serialize(writer, output, options) - | ErrorMessages msgs -> - JsonSerializer.Serialize(writer, msgs, options) - | CustomResponse jsonDocument -> - jsonDocument.WriteTo(writer) - - writer.WriteEndObject() +type RawServerMessageConverter () = + inherit JsonConverter () + + override __.Read (reader : byref, typeToConvert : Type, options : JsonSerializerOptions) : RawServerMessage = + failwith "deserializing a RawServerMessage is not supported (yet(?))" + + override __.Write (writer : Utf8JsonWriter, value : RawServerMessage, options : JsonSerializerOptions) = + writer.WriteStartObject () + writer.WriteString ("type", value.Type) + match value.Id with + | None -> () + | Some id -> writer.WriteString ("id", id) + + match value.Payload with + | None -> () + | Some serverRawPayload -> + match serverRawPayload with + | ExecutionResult output -> + writer.WritePropertyName ("payload") + JsonSerializer.Serialize (writer, output, options) + | ErrorMessages msgs -> JsonSerializer.Serialize (writer, msgs, options) + | CustomResponse jsonDocument -> jsonDocument.WriteTo (writer) + + writer.WriteEndObject () module JsonConverterUtils = - let [] UnionTag = "kind" - - let private defaultJsonFSharpOptions = - JsonFSharpOptions( - JsonUnionEncoding.InternalTag - ||| JsonUnionEncoding.AllowUnorderedTag - ||| JsonUnionEncoding.NamedFields - ||| JsonUnionEncoding.UnwrapSingleCaseUnions - ||| JsonUnionEncoding.UnwrapRecordCases - ||| JsonUnionEncoding.UnwrapOption - ||| JsonUnionEncoding.UnwrapFieldlessTags, - UnionTag, - allowOverride = true) - let configureSerializer (executor : Executor<'Root>) (jsonSerializerOptions : JsonSerializerOptions) = - jsonSerializerOptions.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase - jsonSerializerOptions.PropertyNameCaseInsensitive <- true - jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()) - jsonSerializerOptions.Converters.Add(new ClientMessageConverter<'Root>(executor)) - jsonSerializerOptions.Converters.Add(new RawServerMessageConverter()) - jsonSerializerOptions |> defaultJsonFSharpOptions.AddToJsonSerializerOptions - jsonSerializerOptions \ No newline at end of file + [] + let UnionTag = "kind" + + let private defaultJsonFSharpOptions = + JsonFSharpOptions ( + JsonUnionEncoding.InternalTag + ||| JsonUnionEncoding.AllowUnorderedTag + ||| JsonUnionEncoding.NamedFields + ||| JsonUnionEncoding.UnwrapSingleCaseUnions + ||| JsonUnionEncoding.UnwrapRecordCases + ||| JsonUnionEncoding.UnwrapOption + ||| JsonUnionEncoding.UnwrapFieldlessTags, + UnionTag, + allowOverride = true + ) + let configureSerializer (executor : Executor<'Root>) (jsonSerializerOptions : JsonSerializerOptions) = + jsonSerializerOptions.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase + jsonSerializerOptions.PropertyNameCaseInsensitive <- true + jsonSerializerOptions.Converters.Add (new JsonStringEnumConverter ()) + jsonSerializerOptions.Converters.Add (new ClientMessageConverter<'Root> (executor)) + jsonSerializerOptions.Converters.Add (new RawServerMessageConverter ()) + jsonSerializerOptions + |> defaultJsonFSharpOptions.AddToJsonSerializerOptions + jsonSerializerOptions diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index c4182346d..4a430b1cc 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -8,34 +8,44 @@ open System.Text.Json open Microsoft.AspNetCore.Http [] -type ServiceCollectionExtensions() = +type ServiceCollectionExtensions () = - static let createStandardOptions executor rootFactory endpointUrl = - { SchemaExecutor = executor - RootFactory = rootFactory - SerializerOptions = - JsonSerializerOptions(IgnoreNullValues = true) - |> JsonConverterUtils.configureSerializer executor - WebsocketOptions = - { EndpointUrl = endpointUrl - ConnectionInitTimeoutInMs = 3000 - CustomPingHandler = None } + static let createStandardOptions executor rootFactory endpointUrl = { + SchemaExecutor = executor + RootFactory = rootFactory + SerializerOptions = + JsonSerializerOptions (IgnoreNullValues = true) + |> JsonConverterUtils.configureSerializer executor + WebsocketOptions = { + EndpointUrl = endpointUrl + ConnectionInitTimeoutInMs = 3000 + CustomPingHandler = None + } } - [] - static member AddGraphQLOptions<'Root>(this : IServiceCollection, executor : Executor<'Root>, rootFactory : HttpContext -> 'Root, endpointUrl : string) = - this.AddSingleton>(createStandardOptions executor rootFactory endpointUrl) + [] + static member AddGraphQLOptions<'Root> + ( + this : IServiceCollection, + executor : Executor<'Root>, + rootFactory : HttpContext -> 'Root, + endpointUrl : string + ) = + this.AddSingleton> (createStandardOptions executor rootFactory endpointUrl) - [] - static member AddGraphQLOptionsWith<'Root> - ( this : IServiceCollection, - executor : Executor<'Root>, - rootFactory : HttpContext -> 'Root, - endpointUrl : string, - extraConfiguration : GraphQLOptions<'Root> -> GraphQLOptions<'Root> - ) = - this.AddSingleton>(createStandardOptions executor rootFactory endpointUrl |> extraConfiguration) + [] + static member AddGraphQLOptionsWith<'Root> + ( + this : IServiceCollection, + executor : Executor<'Root>, + rootFactory : HttpContext -> 'Root, + endpointUrl : string, + extraConfiguration : GraphQLOptions<'Root> -> GraphQLOptions<'Root> + ) = + this.AddSingleton> ( + createStandardOptions executor rootFactory endpointUrl + |> extraConfiguration + ) - [] - static member UseWebSocketsForGraphQL<'Root>(this : IApplicationBuilder) = - this.UseMiddleware>() \ No newline at end of file + [] + static member UseWebSocketsForGraphQL<'Root> (this : IApplicationBuilder) = this.UseMiddleware> () diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs index 39a624309..10456be60 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs @@ -6,33 +6,26 @@ open System.Text.Json open Xunit let toClientMessage (theInput : string) = - let serializerOptions = new JsonSerializerOptions() + let serializerOptions = new JsonSerializerOptions () serializerOptions.PropertyNameCaseInsensitive <- true - serializerOptions.Converters.Add(new ClientMessageConverter(TestSchema.executor)) - serializerOptions.Converters.Add(new RawServerMessageConverter()) - JsonSerializer.Deserialize(theInput, serializerOptions) + serializerOptions.Converters.Add (new ClientMessageConverter (TestSchema.executor)) + serializerOptions.Converters.Add (new RawServerMessageConverter ()) + JsonSerializer.Deserialize (theInput, serializerOptions) let willResultInInvalidMessage expectedExplanation input = try - let result = - input - |> toClientMessage - Assert.Fail(sprintf "should have failed, but succeeded with result: '%A'" result) + let result = input |> toClientMessage + Assert.Fail (sprintf "should have failed, but succeeded with result: '%A'" result) with - | :? JsonException as ex -> - Assert.Equal(expectedExplanation, ex.Message) - | :? InvalidMessageException as ex -> - Assert.Equal(expectedExplanation, ex.Message) + | :? JsonException as ex -> Assert.Equal (expectedExplanation, ex.Message) + | :? InvalidMessageException as ex -> Assert.Equal (expectedExplanation, ex.Message) let willResultInJsonException input = try - input - |> toClientMessage - |> ignore - Assert.Fail("expected that a JsonException would have already been thrown at this point") - with - | :? JsonException as ex -> - Assert.True(true) + input |> toClientMessage |> ignore + Assert.Fail ("expected that a JsonException would have already been thrown at this point") + with :? JsonException as ex -> + Assert.True (true) [] let ``Unknown message type will result in invalid message`` () = @@ -77,7 +70,8 @@ let ``Payload type of number in subscribe message will result in invalid message "payload": 42 } """ - |> willResultInInvalidMessage "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AspNetCore.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 2." + |> willResultInInvalidMessage + "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AspNetCore.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 2." [] let ``No id in subscribe message will result in invalid message`` () = @@ -98,7 +92,8 @@ let ``String payload wrongly used in subscribe will result in invalid message`` "payload": "{\"query\": \"subscription { watchMoon(id: \\\"1\\\") { id name isMoon } }\"}" } """ - |> willResultInInvalidMessage "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AspNetCore.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 79." + |> willResultInInvalidMessage + "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AspNetCore.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 79." [] let ``Id is incorrectly a number in a subscribe message will result in JsonException`` () = @@ -140,8 +135,3 @@ let ``Complete message with a null id will result in invalid message`` () = } """ |> willResultInInvalidMessage "property \"id\" is required for this message but was not present." - - - - - diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs index a1e99b7fe..9ebf2d1de 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs @@ -6,106 +6,98 @@ open System.Text.Json open Xunit let getStdSerializerOptions () = - let serializerOptions = new JsonSerializerOptions() + let serializerOptions = new JsonSerializerOptions () serializerOptions.PropertyNameCaseInsensitive <- true - serializerOptions.Converters.Add(new ClientMessageConverter(TestSchema.executor)) - serializerOptions.Converters.Add(new RawServerMessageConverter()) + serializerOptions.Converters.Add (new ClientMessageConverter (TestSchema.executor)) + serializerOptions.Converters.Add (new RawServerMessageConverter ()) serializerOptions [] let ``Deserializes ConnectionInit correctly`` () = - let serializerOptions = getStdSerializerOptions() + let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"connection_init\"}" - let result = JsonSerializer.Deserialize(input, serializerOptions) + let result = JsonSerializer.Deserialize (input, serializerOptions) match result with | ConnectionInit None -> () // <-- expected - | other -> - Assert.Fail($"unexpected actual value: '%A{other}'") + | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") [] let ``Deserializes ConnectionInit with payload correctly`` () = - let serializerOptions = getStdSerializerOptions() + let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"connection_init\", \"payload\":\"hello\"}" - let result = JsonSerializer.Deserialize(input, serializerOptions) + let result = JsonSerializer.Deserialize (input, serializerOptions) match result with | ConnectionInit _ -> () // <-- expected - | other -> - Assert.Fail($"unexpected actual value: '%A{other}'") + | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") [] let ``Deserializes ClientPing correctly`` () = - let serializerOptions = getStdSerializerOptions() + let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"ping\"}" - let result = JsonSerializer.Deserialize(input, serializerOptions) + let result = JsonSerializer.Deserialize (input, serializerOptions) match result with | ClientPing None -> () // <-- expected - | other -> - Assert.Fail($"unexpected actual value '%A{other}'") + | other -> Assert.Fail ($"unexpected actual value '%A{other}'") [] let ``Deserializes ClientPing with payload correctly`` () = - let serializerOptions = getStdSerializerOptions() + let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"ping\", \"payload\":\"ping!\"}" - let result = JsonSerializer.Deserialize(input, serializerOptions) + let result = JsonSerializer.Deserialize (input, serializerOptions) match result with | ClientPing _ -> () // <-- expected - | other -> - Assert.Fail($"unexpected actual value '%A{other}'") + | other -> Assert.Fail ($"unexpected actual value '%A{other}'") [] let ``Deserializes ClientPong correctly`` () = - let serializerOptions = getStdSerializerOptions() + let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"pong\"}" - let result = JsonSerializer.Deserialize(input, serializerOptions) + let result = JsonSerializer.Deserialize (input, serializerOptions) match result with | ClientPong None -> () // <-- expected - | other -> - Assert.Fail($"unexpected actual value: '%A{other}'") + | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") [] let ``Deserializes ClientPong with payload correctly`` () = - let serializerOptions = getStdSerializerOptions() + let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"pong\", \"payload\": \"pong!\"}" - - let result = JsonSerializer.Deserialize(input, serializerOptions) + + let result = JsonSerializer.Deserialize (input, serializerOptions) match result with | ClientPong _ -> () // <-- expected - | other -> - Assert.Fail($"unexpected actual value: '%A{other}'") + | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") [] -let ``Deserializes ClientComplete correctly``() = - let serializerOptions = getStdSerializerOptions() +let ``Deserializes ClientComplete correctly`` () = + let serializerOptions = getStdSerializerOptions () let input = "{\"id\": \"65fca2b5-f149-4a70-a055-5123dea4628f\", \"type\":\"complete\"}" - let result = JsonSerializer.Deserialize(input, serializerOptions) + let result = JsonSerializer.Deserialize (input, serializerOptions) match result with - | ClientComplete id -> - Assert.Equal("65fca2b5-f149-4a70-a055-5123dea4628f", id) - | other -> - Assert.Fail($"unexpected actual value: '%A{other}'") + | ClientComplete id -> Assert.Equal ("65fca2b5-f149-4a70-a055-5123dea4628f", id) + | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") [] let ``Deserializes client subscription correctly`` () = - let serializerOptions = getStdSerializerOptions() + let serializerOptions = getStdSerializerOptions () let input = """{ @@ -117,41 +109,31 @@ let ``Deserializes client subscription correctly`` () = } """ - let result = JsonSerializer.Deserialize(input, serializerOptions) + let result = JsonSerializer.Deserialize (input, serializerOptions) match result with | Subscribe (id, payload) -> - Assert.Equal("b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", id) - Assert.Equal(1, payload.ExecutionPlan.Operation.SelectionSet.Length) + Assert.Equal ("b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", id) + Assert.Equal (1, payload.ExecutionPlan.Operation.SelectionSet.Length) let watchMoonSelection = payload.ExecutionPlan.Operation.SelectionSet |> List.head match watchMoonSelection with | Field watchMoonField -> - Assert.Equal("watchMoon", watchMoonField.Name) - Assert.Equal(1, watchMoonField.Arguments.Length) + Assert.Equal ("watchMoon", watchMoonField.Name) + Assert.Equal (1, watchMoonField.Arguments.Length) let watchMoonFieldArg = watchMoonField.Arguments |> List.head - Assert.Equal("id", watchMoonFieldArg.Name) + Assert.Equal ("id", watchMoonFieldArg.Name) match watchMoonFieldArg.Value with - | StringValue theValue -> - Assert.Equal("1", theValue) - | other -> - Assert.Fail($"expected arg to be a StringValue, but it was: %A{other}") - Assert.Equal(3, watchMoonField.SelectionSet.Length) + | StringValue theValue -> Assert.Equal ("1", theValue) + | other -> Assert.Fail ($"expected arg to be a StringValue, but it was: %A{other}") + Assert.Equal (3, watchMoonField.SelectionSet.Length) match watchMoonField.SelectionSet.[0] with - | Field firstField -> - Assert.Equal("id", firstField.Name) - | other -> - Assert.Fail($"expected field to be a Field, but it was: %A{other}") + | Field firstField -> Assert.Equal ("id", firstField.Name) + | other -> Assert.Fail ($"expected field to be a Field, but it was: %A{other}") match watchMoonField.SelectionSet.[1] with - | Field secondField -> - Assert.Equal("name", secondField.Name) - | other -> - Assert.Fail($"expected field to be a Field, but it was: %A{other}") + | Field secondField -> Assert.Equal ("name", secondField.Name) + | other -> Assert.Fail ($"expected field to be a Field, but it was: %A{other}") match watchMoonField.SelectionSet.[2] with - | Field thirdField -> - Assert.Equal("isMoon", thirdField.Name) - | other -> - Assert.Fail($"expected field to be a Field, but it was: %A{other}") - | somethingElse -> - Assert.Fail($"expected it to be a field, but it was: %A{somethingElse}") - | other -> - Assert.Fail($"unexpected actual value: '%A{other}'") \ No newline at end of file + | Field thirdField -> Assert.Equal ("isMoon", thirdField.Name) + | other -> Assert.Fail ($"expected field to be a Field, but it was: %A{other}") + | somethingElse -> Assert.Fail ($"expected it to be a field, but it was: %A{somethingElse}") + | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs index e44f0ca73..a7899c66f 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/TestSchema.fs @@ -10,220 +10,273 @@ type Episode = | Empire = 2 | Jedi = 3 -type Human = - { Id : string - Name : string option - Friends : string list - AppearsIn : Episode list - HomePlanet : string option } - -type Droid = - { Id : string - Name : string option - Friends : string list - AppearsIn : Episode list - PrimaryFunction : string option } - -type Planet = - { Id : string - Name : string option - mutable IsMoon : bool option } +type Human = { + Id : string + Name : string option + Friends : string list + AppearsIn : Episode list + HomePlanet : string option +} + +type Droid = { + Id : string + Name : string option + Friends : string list + AppearsIn : Episode list + PrimaryFunction : string option +} + +type Planet = { + Id : string + Name : string option + mutable IsMoon : bool option +} with + member x.SetMoon b = x.IsMoon <- b x -type Root = - { RequestId: string } +type Root = { RequestId : string } type Character = | Human of Human | Droid of Droid module TestSchema = - let humans = - [ { Id = "1000" + let humans = [ + { + Id = "1000" Name = Some "Luke Skywalker" Friends = [ "1002"; "1003"; "2000"; "2001" ] AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] - HomePlanet = Some "Tatooine" } - { Id = "1001" + HomePlanet = Some "Tatooine" + } + { + Id = "1001" Name = Some "Darth Vader" Friends = [ "1004" ] AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] - HomePlanet = Some "Tatooine" } - { Id = "1002" + HomePlanet = Some "Tatooine" + } + { + Id = "1002" Name = Some "Han Solo" Friends = [ "1000"; "1003"; "2001" ] AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] - HomePlanet = None } - { Id = "1003" + HomePlanet = None + } + { + Id = "1003" Name = Some "Leia Organa" Friends = [ "1000"; "1002"; "2000"; "2001" ] AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] - HomePlanet = Some "Alderaan" } - { Id = "1004" + HomePlanet = Some "Alderaan" + } + { + Id = "1004" Name = Some "Wilhuff Tarkin" Friends = [ "1001" ] AppearsIn = [ Episode.NewHope ] - HomePlanet = None } ] + HomePlanet = None + } + ] - let droids = - [ { Id = "2000" + let droids = [ + { + Id = "2000" Name = Some "C-3PO" Friends = [ "1000"; "1002"; "1003"; "2001" ] AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] - PrimaryFunction = Some "Protocol" } - { Id = "2001" + PrimaryFunction = Some "Protocol" + } + { + Id = "2001" Name = Some "R2-D2" Friends = [ "1000"; "1002"; "1003" ] AppearsIn = [ Episode.NewHope; Episode.Empire; Episode.Jedi ] - PrimaryFunction = Some "Astromech" } ] + PrimaryFunction = Some "Astromech" + } + ] - let planets = - [ { Id = "1" - Name = Some "Tatooine" - IsMoon = Some false} - { Id = "2" - Name = Some "Endor" - IsMoon = Some true} - { Id = "3" - Name = Some "Death Star" - IsMoon = Some false}] + let planets = [ + { Id = "1"; Name = Some "Tatooine"; IsMoon = Some false } + { Id = "2"; Name = Some "Endor"; IsMoon = Some true } + { Id = "3"; Name = Some "Death Star"; IsMoon = Some false } + ] - let getHuman id = - humans |> List.tryFind (fun h -> h.Id = id) + let getHuman id = humans |> List.tryFind (fun h -> h.Id = id) - let getDroid id = - droids |> List.tryFind (fun d -> d.Id = id) + let getDroid id = droids |> List.tryFind (fun d -> d.Id = id) - let getPlanet id = - planets |> List.tryFind (fun p -> p.Id = id) + let getPlanet id = planets |> List.tryFind (fun p -> p.Id = id) - let characters = - (humans |> List.map Human) @ (droids |> List.map Droid) + let characters = (humans |> List.map Human) @ (droids |> List.map Droid) - let matchesId id = function + let matchesId id = + function | Human h -> h.Id = id | Droid d -> d.Id = id - let getCharacter id = - characters |> List.tryFind (matchesId id) + let getCharacter id = characters |> List.tryFind (matchesId id) let EpisodeType = - Define.Enum( + Define.Enum ( name = "Episode", description = "One of the films in the Star Wars Trilogy.", options = [ - Define.EnumValue("NewHope", Episode.NewHope, "Released in 1977.") - Define.EnumValue("Empire", Episode.Empire, "Released in 1980.") - Define.EnumValue("Jedi", Episode.Jedi, "Released in 1983.") ]) + Define.EnumValue ("NewHope", Episode.NewHope, "Released in 1977.") + Define.EnumValue ("Empire", Episode.Empire, "Released in 1980.") + Define.EnumValue ("Jedi", Episode.Jedi, "Released in 1983.") + ] + ) let rec CharacterType = - Define.Union( + Define.Union ( name = "Character", description = "A character in the Star Wars Trilogy.", options = [ HumanType; DroidType ], - resolveValue = (fun o -> - match o with - | Human h -> box h - | Droid d -> upcast d), - resolveType = (fun o -> - match o with - | Human _ -> upcast HumanType - | Droid _ -> upcast DroidType)) + resolveValue = + (fun o -> + match o with + | Human h -> box h + | Droid d -> upcast d), + resolveType = + (fun o -> + match o with + | Human _ -> upcast HumanType + | Droid _ -> upcast DroidType) + ) and HumanType : ObjectDef = - Define.Object( + Define.Object ( name = "Human", description = "A humanoid creature in the Star Wars universe.", isTypeOf = (fun o -> o :? Human), - fieldsFn = fun () -> - [ - Define.Field("id", StringType, "The id of the human.", fun _ (h : Human) -> h.Id) - Define.Field("name", Nullable StringType, "The name of the human.", fun _ (h : Human) -> h.Name) - Define.Field("friends", ListOf (Nullable CharacterType), "The friends of the human, or an empty list if they have none.", - fun _ (h : Human) -> h.Friends |> List.map getCharacter |> List.toSeq) - Define.Field("appearsIn", ListOf EpisodeType, "Which movies they appear in.", fun _ (h : Human) -> h.AppearsIn) - Define.Field("homePlanet", Nullable StringType, "The home planet of the human, or null if unknown.", fun _ h -> h.HomePlanet) - ]) + fieldsFn = + fun () -> [ + Define.Field ("id", StringType, "The id of the human.", (fun _ (h : Human) -> h.Id)) + Define.Field ("name", Nullable StringType, "The name of the human.", (fun _ (h : Human) -> h.Name)) + Define.Field ( + "friends", + ListOf (Nullable CharacterType), + "The friends of the human, or an empty list if they have none.", + fun _ (h : Human) -> h.Friends |> List.map getCharacter |> List.toSeq + ) + Define.Field ("appearsIn", ListOf EpisodeType, "Which movies they appear in.", (fun _ (h : Human) -> h.AppearsIn)) + Define.Field ("homePlanet", Nullable StringType, "The home planet of the human, or null if unknown.", (fun _ h -> h.HomePlanet)) + ] + ) and DroidType = - Define.Object( + Define.Object ( name = "Droid", description = "A mechanical creature in the Star Wars universe.", isTypeOf = (fun o -> o :? Droid), - fieldsFn = fun () -> - [ - Define.Field("id", StringType, "The id of the droid.", fun _ (d : Droid) -> d.Id) - Define.Field("name", Nullable StringType, "The name of the Droid.", fun _ (d : Droid) -> d.Name) - Define.Field("friends", ListOf (Nullable CharacterType), "The friends of the Droid, or an empty list if they have none.", - fun _ (d : Droid) -> d.Friends |> List.map getCharacter |> List.toSeq) - Define.Field("appearsIn", ListOf EpisodeType, "Which movies they appear in.", fun _ d -> d.AppearsIn) - Define.Field("primaryFunction", Nullable StringType, "The primary function of the droid.", fun _ d -> d.PrimaryFunction) - ]) + fieldsFn = + fun () -> [ + Define.Field ("id", StringType, "The id of the droid.", (fun _ (d : Droid) -> d.Id)) + Define.Field ("name", Nullable StringType, "The name of the Droid.", (fun _ (d : Droid) -> d.Name)) + Define.Field ( + "friends", + ListOf (Nullable CharacterType), + "The friends of the Droid, or an empty list if they have none.", + fun _ (d : Droid) -> d.Friends |> List.map getCharacter |> List.toSeq + ) + Define.Field ("appearsIn", ListOf EpisodeType, "Which movies they appear in.", (fun _ d -> d.AppearsIn)) + Define.Field ("primaryFunction", Nullable StringType, "The primary function of the droid.", (fun _ d -> d.PrimaryFunction)) + ] + ) and PlanetType = - Define.Object( + Define.Object ( name = "Planet", description = "A planet in the Star Wars universe.", isTypeOf = (fun o -> o :? Planet), - fieldsFn = fun () -> - [ - Define.Field("id", StringType, "The id of the planet", fun _ p -> p.Id) - Define.Field("name", Nullable StringType, "The name of the planet.", fun _ p -> p.Name) - Define.Field("isMoon", Nullable BooleanType, "Is that a moon?", fun _ p -> p.IsMoon) - ]) + fieldsFn = + fun () -> [ + Define.Field ("id", StringType, "The id of the planet", (fun _ p -> p.Id)) + Define.Field ("name", Nullable StringType, "The name of the planet.", (fun _ p -> p.Name)) + Define.Field ("isMoon", Nullable BooleanType, "Is that a moon?", (fun _ p -> p.IsMoon)) + ] + ) and RootType = - Define.Object( + Define.Object ( name = "Root", description = "The Root type to be passed to all our resolvers.", isTypeOf = (fun o -> o :? Root), - fieldsFn = fun () -> - [ - Define.Field("requestId", StringType, "The ID of the client.", fun _ (r : Root) -> r.RequestId) - ]) + fieldsFn = + fun () -> [ + Define.Field ("requestId", StringType, "The ID of the client.", (fun _ (r : Root) -> r.RequestId)) + ] + ) let Query = - Define.Object( + Define.Object ( name = "Query", fields = [ - Define.Field("hero", Nullable HumanType, "Gets human hero", [ Define.Input("id", StringType) ], fun ctx _ -> getHuman (ctx.Arg("id"))) - Define.Field("droid", Nullable DroidType, "Gets droid", [ Define.Input("id", StringType) ], fun ctx _ -> getDroid (ctx.Arg("id"))) - Define.Field("planet", Nullable PlanetType, "Gets planet", [ Define.Input("id", StringType) ], fun ctx _ -> getPlanet (ctx.Arg("id"))) - Define.Field("characters", ListOf CharacterType, "Gets characters", fun _ _ -> characters) ]) + Define.Field ( + "hero", + Nullable HumanType, + "Gets human hero", + [ Define.Input ("id", StringType) ], + fun ctx _ -> getHuman (ctx.Arg ("id")) + ) + Define.Field ( + "droid", + Nullable DroidType, + "Gets droid", + [ Define.Input ("id", StringType) ], + fun ctx _ -> getDroid (ctx.Arg ("id")) + ) + Define.Field ( + "planet", + Nullable PlanetType, + "Gets planet", + [ Define.Input ("id", StringType) ], + fun ctx _ -> getPlanet (ctx.Arg ("id")) + ) + Define.Field ("characters", ListOf CharacterType, "Gets characters", (fun _ _ -> characters)) + ] + ) let Subscription = - Define.SubscriptionObject( + Define.SubscriptionObject ( name = "Subscription", fields = [ - Define.SubscriptionField( + Define.SubscriptionField ( "watchMoon", RootType, PlanetType, "Watches to see if a planet is a moon.", - [ Define.Input("id", StringType) ], - (fun ctx _ p -> if ctx.Arg("id") = p.Id then Some p else None)) ]) + [ Define.Input ("id", StringType) ], + (fun ctx _ p -> if ctx.Arg ("id") = p.Id then Some p else None) + ) + ] + ) let schemaConfig = SchemaConfig.Default let Mutation = - Define.Object( + Define.Object ( name = "Mutation", fields = [ - Define.Field( + Define.Field ( "setMoon", Nullable PlanetType, "Defines if a planet is actually a moon or not.", - [ Define.Input("id", StringType); Define.Input("isMoon", BooleanType) ], + [ Define.Input ("id", StringType); Define.Input ("isMoon", BooleanType) ], fun ctx _ -> - getPlanet (ctx.Arg("id")) + getPlanet (ctx.Arg ("id")) |> Option.map (fun x -> - x.SetMoon(Some(ctx.Arg("isMoon"))) |> ignore + x.SetMoon (Some (ctx.Arg ("isMoon"))) |> ignore schemaConfig.SubscriptionProvider.Publish "watchMoon" x schemaConfig.LiveFieldSubscriptionProvider.Publish "Planet" "isMoon" x - x))]) + x) + ) + ] + ) - let schema : ISchema = upcast Schema(Query, Mutation, Subscription, schemaConfig) + let schema : ISchema = upcast Schema (Query, Mutation, Subscription, schemaConfig) - let executor = Executor(schema, []) \ No newline at end of file + let executor = Executor (schema, []) From 318901d2e6b28ab3fe92789a8706953e3818e00c Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 6 Mar 2024 20:51:26 +0100 Subject: [PATCH 068/100] Removed superfluous JsonOptions configuration --- samples/star-wars-api/Startup.fs | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/samples/star-wars-api/Startup.fs b/samples/star-wars-api/Startup.fs index a8e66c326..fd4132a21 100644 --- a/samples/star-wars-api/Startup.fs +++ b/samples/star-wars-api/Startup.fs @@ -5,20 +5,12 @@ open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe open FSharp.Data.GraphQL.Server.AspNetCore open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http -open Microsoft.AspNetCore.Http.Json open Microsoft.AspNetCore.Server.Kestrel.Core open Microsoft.Extensions.Configuration open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging open System -open Microsoft.AspNetCore.Server.Kestrel.Core open Microsoft.Extensions.Hosting -open Microsoft.Extensions.Options - -// See https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions -type MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions -// See https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.json.jsonoptions -type HttpClientJsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions module Constants = let [] Indented = "Indented" @@ -36,25 +28,6 @@ type Startup private () = .AddGiraffe() .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) - // Surprisingly minimal APIs use Microsoft.AspNetCore.Http.Json.JsonOptions - // Use if you want to return HTTP responses using minmal APIs IResult interface - .Configure( - Action(fun o -> - Json.configureDefaultSerializerOptions Seq.empty o.SerializerOptions - ) - ) - // Use for pretty printing in logs - .Configure( - Constants.Indented, - Action(fun o -> - Json.configureDefaultSerializerOptions Seq.empty o.SerializerOptions - o.SerializerOptions.WriteIndented <- true - ) - ) - // Replace Newtonsoft.Json and use the same settings in Giraffe - .AddSingleton(fun sp -> - let options = sp.GetService>() - SystemTextJson.Serializer(options.Value.SerializerOptions)) .AddGraphQLOptions( Schema.executor, rootFactory, From 390e8930164f6414f91f1607f4d32d1543540efb Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 6 Mar 2024 21:30:14 +0100 Subject: [PATCH 069/100] samples: removing KestrelServerOptions.AllowSynchronousIO <- true... As it seems to have become obsolete now. According to suggestion in pull request. --- samples/chat-app/server/Program.fs | 1 - samples/star-wars-api/Startup.fs | 2 -- 2 files changed, 3 deletions(-) diff --git a/samples/chat-app/server/Program.fs b/samples/chat-app/server/Program.fs index 786865406..76297ed45 100644 --- a/samples/chat-app/server/Program.fs +++ b/samples/chat-app/server/Program.fs @@ -24,7 +24,6 @@ module Program = let builder = WebApplication.CreateBuilder (args) builder.Services .AddGiraffe() - .Configure(Action (fun x -> x.AllowSynchronousIO <- true)) .AddGraphQLOptions (Schema.executor, rootFactory, "/ws") |> ignore diff --git a/samples/star-wars-api/Startup.fs b/samples/star-wars-api/Startup.fs index fd4132a21..be9ce7230 100644 --- a/samples/star-wars-api/Startup.fs +++ b/samples/star-wars-api/Startup.fs @@ -26,8 +26,6 @@ type Startup private () = member _.ConfigureServices(services: IServiceCollection) = services .AddGiraffe() - .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) - .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) .AddGraphQLOptions( Schema.executor, rootFactory, From d6f961191e1443f0a3fd464589cecb46b75fc080 Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 6 Mar 2024 21:32:40 +0100 Subject: [PATCH 070/100] samples/chat-app: undoing wrong changes to launchSetting.json The suggestion in the pull request was wrong. The values "http" and "https" are not supported as values for `commandName` and `dotnet run --project ...` won't run because of that. --- samples/chat-app/server/Properties/launchSettings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/chat-app/server/Properties/launchSettings.json b/samples/chat-app/server/Properties/launchSettings.json index 38cd775cc..40fef14da 100644 --- a/samples/chat-app/server/Properties/launchSettings.json +++ b/samples/chat-app/server/Properties/launchSettings.json @@ -9,7 +9,7 @@ }, "profiles": { "http": { - "commandName": "http", + "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "http://localhost:5092", @@ -18,7 +18,7 @@ } }, "https": { - "commandName": "https", + "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:7122;http://localhost:5092", From 9b2903718da09a98662c18782921436f4ed08b30 Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 6 Mar 2024 21:48:50 +0100 Subject: [PATCH 071/100] .IntegrationTests.Server: removed some now superfluous settings --- .../Startup.fs | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs index 754040a9e..c73a47ebd 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs @@ -3,11 +3,9 @@ namespace FSharp.Data.GraphQL.IntegrationTests.Server open System open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http -open Microsoft.AspNetCore.Server.Kestrel.Core open Microsoft.Extensions.Configuration open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging -open Microsoft.Extensions.Options open Giraffe open FSharp.Data.GraphQL.Server.AspNetCore @@ -15,11 +13,6 @@ open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe open FSharp.Data.GraphQL.Samples.StarWarsApi open Microsoft.Extensions.Hosting -// See https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions -type MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions -// See https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.json.jsonoptions -type HttpClientJsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions - module Constants = let [] Indented = "Indented" @@ -35,33 +28,11 @@ type Startup private () = member __.ConfigureServices(services: IServiceCollection) = services .AddGiraffe() - .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) - .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) .AddGraphQLOptions( Schema.executor, rootFactory, "/ws" ) - // Surprisingly minimal APIs use Microsoft.AspNetCore.Http.Json.JsonOptions - // Use if you want to return HTTP responses using minmal APIs IResult interface - .Configure( - Action(fun o -> - Json.configureDefaultSerializerOptions Seq.empty o.SerializerOptions - ) - ) - // // Use for pretty printing in logs - .Configure( - Constants.Indented, - Action(fun o -> - Json.configureDefaultSerializerOptions Seq.empty o.SerializerOptions - o.SerializerOptions.WriteIndented <- true - ) - ) - // Replace Newtonsoft.Json and use the same settings in Giraffe - .AddSingleton(fun sp -> - let options = sp.GetService>() - SystemTextJson.Serializer(options.Value.SerializerOptions)) - |> ignore member __.Configure(app: IApplicationBuilder) = From 017cdd7b64b1e117d60b93d1b7eaeaa871d770cc Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 6 Mar 2024 21:55:31 +0100 Subject: [PATCH 072/100] .Server.AspNetCore/README.md: updated the sample snippet to... to reflect current usage. --- src/FSharp.Data.GraphQL.Server.AspNetCore/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/README.md b/src/FSharp.Data.GraphQL.Server.AspNetCore/README.md index fbec7edb0..547f9d9a3 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/README.md +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/README.md @@ -29,7 +29,6 @@ type Startup private () = member _.ConfigureServices(services: IServiceCollection) = services.AddGiraffe() - .Configure(Action(fun x -> x.AllowSynchronousIO <- true)) .AddGraphQLOptions( // STEP 1: Setting the options Schema.executor, // --> Schema.executor is defined by you somewhere else (in another file) rootFactory, @@ -48,7 +47,7 @@ type Startup private () = .UseGiraffe (HttpHandlers.handleGraphQL applicationLifetime.ApplicationStopping - (loggerFactory.CreateLogger("HttpHandlers.handlerGraphQL")) + (loggerFactory.CreateLogger("FSharp.Data.GraphQL.Server.AspNetCore.HttpHandlers.handleGraphQL")) ) member val Configuration : IConfiguration = null with get, set @@ -94,7 +93,7 @@ Don't forget to notify subscribers about new values: Finally run the server (e.g. make it listen at `localhost:8086`). There's a demo chat application backend in the `samples/chat-app` folder that showcases the use of `FSharp.Data.GraphQL.Server.AspNetCore` in a real-time application scenario, that is: with usage of GraphQL subscriptions (but not only). -The tried and trusted `star-wars-api` also shows how to use subscriptions, but is a more basic example. As a side note, the implementation in `star-wars-api` was used as a starting point for the development of `FSharp.Data.GraphQL.Server.AspNetCore`. +The tried and trusted `star-wars-api` also shows how to use subscriptions, but is a more basic example in that regard. As a side note, the implementation in `star-wars-api` was used as a starting point for the development of `FSharp.Data.GraphQL.Server.AspNetCore`. ### Client Using your favorite (or not :)) client library (e.g.: [Apollo Client](https://www.apollographql.com/docs/react/get-started), [Relay](https://relay.dev), [Strawberry Shake](https://chillicream.com/docs/strawberryshake/v13), [elm-graphql](https://github.com/dillonkearns/elm-graphql) ❤️), just point to `localhost:8086/graphql` (as per the example above) and, as long as the client implements the `graphql-transport-ws` subprotocol, subscriptions should work. From afa2b7a1eeacd1588d91413fca1e8088fb46eb4a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 15:33:55 +0400 Subject: [PATCH 073/100] Added ability to get idented `JsonSerializerOptions` --- .../GraphQLOptions.fs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs index 11459e4d0..bb49bbff2 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs @@ -19,4 +19,9 @@ type GraphQLOptions<'Root> = { RootFactory : HttpContext -> 'Root SerializerOptions : JsonSerializerOptions WebsocketOptions : GraphQLTransportWSOptions -} +} with + + member options.GetSerializerOptionsIdented () = + let options = JsonSerializerOptions (options.SerializerOptions) + options.WriteIndented <- true + options From dfdb263c7700e9e1a22fd9c183687d7e313fcba2 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 15:55:45 +0400 Subject: [PATCH 074/100] Moved chat-app near to starwars-api --- Directory.Build.props | 4 +--- FSharp.Data.GraphQL.sln | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 5087e944d..829f6d106 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,12 +26,10 @@ https://fsprojects.github.io/FSharp.Data.GraphQL false MIT - true true snupkg + true true - - v diff --git a/FSharp.Data.GraphQL.sln b/FSharp.Data.GraphQL.sln index 4b47d839b..0adc99af1 100644 --- a/FSharp.Data.GraphQL.sln +++ b/FSharp.Data.GraphQL.sln @@ -40,6 +40,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "star-wars-api", "star-wars- EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Samples.StarWarsApi", "samples\star-wars-api\FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj", "{B837B3ED-83CE-446F-A4E5-44CB06AA6505}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "chat-app", "chat-app", "{24AB1F5A-4996-4DDA-87E0-B82B3A24C13F}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.GraphQL.Samples.ChatApp", "samples\chat-app\server\FSharp.Data.GraphQL.Samples.ChatApp.fsproj", "{225B0790-C6B6-425C-9093-F359A4C635D3}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BEFD8748-2467-45F9-A4AD-B450B12D5F78}" EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Shared", "src\FSharp.Data.GraphQL.Shared\FSharp.Data.GraphQL.Shared.fsproj", "{6768EA38-1335-4B8E-BC09-CCDED1F9AAF6}" @@ -170,10 +174,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "components", "components", samples\relay-modern-starter-kit\src\components\user.jsx = samples\relay-modern-starter-kit\src\components\user.jsx EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "chat-app", "chat-app", "{24AB1F5A-4996-4DDA-87E0-B82B3A24C13F}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.GraphQL.Samples.ChatApp", "samples\chat-app\server\FSharp.Data.GraphQL.Samples.ChatApp.fsproj", "{225B0790-C6B6-425C-9093-F359A4C635D3}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 7c2ad5f302849fe77b2751914a83f99fc2f586f4 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 15:56:23 +0400 Subject: [PATCH 075/100] Aligned StarWars API to ChatApp --- samples/star-wars-api/Program.fs | 74 ++++++++++++++++---------------- samples/star-wars-api/Startup.fs | 8 +--- 2 files changed, 38 insertions(+), 44 deletions(-) diff --git a/samples/star-wars-api/Program.fs b/samples/star-wars-api/Program.fs index 8aca32971..7c3d7a624 100644 --- a/samples/star-wars-api/Program.fs +++ b/samples/star-wars-api/Program.fs @@ -1,43 +1,41 @@ -namespace FSharp.Data.GraphQL.Samples.StarWarsApi +module FSharp.Data.GraphQL.Samples.StarWarsApi.Program open Microsoft.AspNetCore open Microsoft.AspNetCore.Hosting open Microsoft.Extensions.Configuration - -module Program = - let exitCode = 0 - - let buildWebHost (args: string array) = - - // Build an initial configuration that takes in both environment and command line settings. - let config = ConfigurationBuilder() - .AddEnvironmentVariables() - .AddCommandLine(args) - .Build() - - // This is done so that an environment specified on the command line with "--environment" is respected, - // when we look for appsettings.*.json files. - let configureAppConfiguration (context: WebHostBuilderContext) (config: IConfigurationBuilder) = - - // The default IFileProvider has the working directory set to the same as the DLL. - // We'll use this to re-set the FileProvider in the configuration builder below. - let fileProvider = ConfigurationBuilder().GetFileProvider() - - // Extract the environment name from the configuration that was already built above - let envName = context.HostingEnvironment.EnvironmentName - // Use the name to find the additional appsettings file, if present. - config.SetFileProvider(fileProvider) - .AddJsonFile("appsettings.json", false, true) - .AddJsonFile($"appsettings.{envName}.json", true) |> ignore - - WebHost - .CreateDefaultBuilder(args) - .UseConfiguration(config) - .ConfigureAppConfiguration(configureAppConfiguration) - .UseStartup() - - [] - let main args = - buildWebHost(args).Build().Run() - exitCode +let exitCode = 0 + +let buildWebHost (args: string array) = + + // Build an initial configuration that takes in both environment and command line settings. + let config = ConfigurationBuilder() + .AddEnvironmentVariables() + .AddCommandLine(args) + .Build() + + // This is done so that an environment specified on the command line with "--environment" is respected, + // when we look for appsettings.*.json files. + let configureAppConfiguration (context: WebHostBuilderContext) (config: IConfigurationBuilder) = + + // The default IFileProvider has the working directory set to the same as the DLL. + // We'll use this to re-set the FileProvider in the configuration builder below. + let fileProvider = ConfigurationBuilder().GetFileProvider() + + // Extract the environment name from the configuration that was already built above + let envName = context.HostingEnvironment.EnvironmentName + // Use the name to find the additional appsettings file, if present. + config.SetFileProvider(fileProvider) + .AddJsonFile("appsettings.json", false, true) + .AddJsonFile($"appsettings.{envName}.json", true) |> ignore + + WebHost + .CreateDefaultBuilder(args) + .UseConfiguration(config) + .ConfigureAppConfiguration(configureAppConfiguration) + .UseStartup() + +[] +let main args = + buildWebHost(args).Build().Run() + exitCode diff --git a/samples/star-wars-api/Startup.fs b/samples/star-wars-api/Startup.fs index be9ce7230..5b65c5625 100644 --- a/samples/star-wars-api/Startup.fs +++ b/samples/star-wars-api/Startup.fs @@ -3,18 +3,14 @@ namespace FSharp.Data.GraphQL.Samples.StarWarsApi open Giraffe open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe open FSharp.Data.GraphQL.Server.AspNetCore +open System open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http -open Microsoft.AspNetCore.Server.Kestrel.Core open Microsoft.Extensions.Configuration open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging -open System open Microsoft.Extensions.Hosting -module Constants = - let [] Indented = "Indented" - type Startup private () = let rootFactory (ctx) : Root = Root(ctx) @@ -51,7 +47,7 @@ type Startup private () = .UseGiraffe (HttpHandlers.handleGraphQLWithResponseInterception applicationLifetime.ApplicationStopping - (loggerFactory.CreateLogger("HttpHandlers.handlerGraphQL")) + (loggerFactory.CreateLogger("FSharp.Data.GraphQL.Server.AspNetCore.HttpHandlers.handleGraphQL")) (setHttpHeader "Request-Type" "Classic")) member val Configuration : IConfiguration = null with get, set From 2888db65a50a522eaf99e029744346468e8bbcd3 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 17:21:38 +0400 Subject: [PATCH 076/100] Aligned code style --- samples/chat-app/server/FakePersistence.fs | 31 +++++++++++----------- samples/chat-app/server/Schema.fs | 6 +++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/samples/chat-app/server/FakePersistence.fs b/samples/chat-app/server/FakePersistence.fs index a6024df7d..e9b2c2f16 100644 --- a/samples/chat-app/server/FakePersistence.fs +++ b/samples/chat-app/server/FakePersistence.fs @@ -3,32 +3,33 @@ namespace FSharp.Data.GraphQL.Samples.ChatApp open System type FakePersistence () = - static let mutable _members = Map.empty - static let mutable _chatMembers = Map.empty - static let mutable _chatRoomMessages = Map.empty - static let mutable _chatRooms = Map.empty - static let mutable _organizations = + + static let mutable members = Map.empty + static let mutable chatMembers = Map.empty + static let mutable chatRoomMessages = Map.empty + static let mutable chatRooms = Map.empty + static let mutable organizations = let newId = OrganizationId (Guid.Parse ("51f823ef-2294-41dc-9f39-a4b9a237317a")) (newId, { Organization_In_Db.Id = newId; Name = "Public"; Members = []; ChatRooms = [] }) |> List.singleton |> Map.ofList static member Members - with get () = _members - and set (v) = _members <- v + with get () = members + and set (v) = members <- v static member ChatMembers - with get () = _chatMembers - and set (v) = _chatMembers <- v + with get () = chatMembers + and set (v) = chatMembers <- v static member ChatRoomMessages - with get () = _chatRoomMessages - and set (v) = _chatRoomMessages <- v + with get () = chatRoomMessages + and set (v) = chatRoomMessages <- v static member ChatRooms - with get () = _chatRooms - and set (v) = _chatRooms <- v + with get () = chatRooms + and set (v) = chatRooms <- v static member Organizations - with get () = _organizations - and set (v) = _organizations <- v + with get () = organizations + and set (v) = organizations <- v diff --git a/samples/chat-app/server/Schema.fs b/samples/chat-app/server/Schema.fs index 3786c9e81..53c45b900 100644 --- a/samples/chat-app/server/Schema.fs +++ b/samples/chat-app/server/Schema.fs @@ -1,13 +1,15 @@ namespace FSharp.Data.GraphQL.Samples.ChatApp +open System +open FsToolkit.ErrorHandling + open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types -open FsToolkit.ErrorHandling -open System type Root = { RequestId : string } module MapFrom = + let memberInDb_To_Member (x : Member_In_Db) : Member = { Id = x.Id; Name = x.Name } let memberInDb_To_MeAsAMember (x : Member_In_Db) : MeAsAMember = { PrivId = x.PrivId; Id = x.Id; Name = x.Name } From b43c29d271cd8f771c34b3275b7bedf3912694a5 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 17:23:28 +0400 Subject: [PATCH 077/100] Consolidated common logic in `FSharp.Data.GraphQL.Server.AspNetCore` simlified some code --- Packages.props | 1 + samples/chat-app/server/Program.fs | 49 +-- ...rp.Data.GraphQL.Samples.StarWarsApi.fsproj | 6 - samples/star-wars-api/Startup.fs | 50 +-- ...harp.Data.GraphQL.Server.AspNetCore.fsproj | 7 +- .../GQLRequest.fs | 2 +- .../Giraffe}/Ast.fs | 2 +- .../Giraffe}/HttpContext.fs | 8 +- .../Giraffe/HttpHandlers.fs | 387 +++++++++++------- .../Giraffe}/Parser.fs | 2 +- .../GraphQLOptions.fs | 10 + .../GraphQLWebsocketMiddleware.fs | 35 +- .../Helpers.fs | 49 +++ .../Messages.fs | 13 +- .../Serialization/GraphQLQueryDecoding.fs | 84 ---- .../Serialization}/JSON.fs | 20 +- .../Serialization/JsonConverters.fs | 54 +-- .../StartupExtensions.fs | 84 ++-- 18 files changed, 457 insertions(+), 406 deletions(-) rename {samples/star-wars-api => src/FSharp.Data.GraphQL.Server.AspNetCore}/GQLRequest.fs (94%) rename {samples/star-wars-api => src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe}/Ast.fs (95%) rename {samples/star-wars-api => src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe}/HttpContext.fs (85%) rename {samples/star-wars-api => src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe}/Parser.fs (83%) create mode 100644 src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs delete mode 100644 src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs rename {samples/star-wars-api => src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization}/JSON.fs (61%) diff --git a/Packages.props b/Packages.props index c60993330..1932e5cd7 100644 --- a/Packages.props +++ b/Packages.props @@ -19,6 +19,7 @@ + diff --git a/samples/chat-app/server/Program.fs b/samples/chat-app/server/Program.fs index 76297ed45..7b1af3c8c 100644 --- a/samples/chat-app/server/Program.fs +++ b/samples/chat-app/server/Program.fs @@ -1,4 +1,4 @@ -namespace FSharp.Data.GraphQL.Samples.ChatApp +module FSharp.Data.GraphQL.Samples.ChatApp.Program open Giraffe open FSharp.Data.GraphQL.Server.AspNetCore @@ -6,42 +6,33 @@ open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe open System open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http -open Microsoft.AspNetCore.Server.Kestrel.Core -open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Hosting open Microsoft.Extensions.Logging -module Program = +let rootFactory (ctx : HttpContext) : Root = { RequestId = ctx.TraceIdentifier } - let rootFactory (ctx : HttpContext) : Root = { RequestId = ctx.TraceIdentifier } +let errorHandler (ex : Exception) (log : ILogger) = + log.LogError (EventId (), ex, "An unhandled exception has occurred while executing this request.") + clearResponse >=> setStatusCode 500 - let errorHandler (ex : Exception) (log : ILogger) = - log.LogError (EventId (), ex, "An unhandled exception has occurred while executing this request.") - clearResponse >=> setStatusCode 500 +[] +let main args = - [] - let main args = - let builder = WebApplication.CreateBuilder (args) - builder.Services - .AddGiraffe() - .AddGraphQLOptions (Schema.executor, rootFactory, "/ws") - |> ignore + let builder = WebApplication.CreateBuilder (args) + builder.Services + .AddGiraffe() + .AddGraphQLOptions (Schema.executor, rootFactory, "/ws") + |> ignore - let app = builder.Build () + let app = builder.Build () - let applicationLifetime = app.Services.GetRequiredService () - let loggerFactory = app.Services.GetRequiredService () - app - .UseGiraffeErrorHandler(errorHandler) - .UseWebSockets() - .UseWebSocketsForGraphQL() - .UseGiraffe ( - HttpHandlers.handleGraphQL - applicationLifetime.ApplicationStopping - (loggerFactory.CreateLogger ("FSharp.Data.GraphQL.Server.AspNetCore.HttpHandlers.handleGraphQL")) - ) + app + .UseGiraffeErrorHandler(errorHandler) + .UseWebSockets() + .UseWebSocketsForGraphQL() + .UseGiraffe (HttpHandlers.graphQL) - app.Run () + app.Run () - 0 // Exit code + 0 // Exit code diff --git a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj index 1e7ee9ed3..09bfd9f9f 100644 --- a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj +++ b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj @@ -8,7 +8,6 @@ - @@ -17,12 +16,7 @@ - - - - - diff --git a/samples/star-wars-api/Startup.fs b/samples/star-wars-api/Startup.fs index 5b65c5625..c6b2625f7 100644 --- a/samples/star-wars-api/Startup.fs +++ b/samples/star-wars-api/Startup.fs @@ -1,8 +1,5 @@ namespace FSharp.Data.GraphQL.Samples.StarWarsApi -open Giraffe -open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe -open FSharp.Data.GraphQL.Server.AspNetCore open System open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http @@ -10,44 +7,47 @@ open Microsoft.Extensions.Configuration open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging open Microsoft.Extensions.Hosting +open Giraffe +open FSharp.Data.GraphQL.Server.AspNetCore +open FSharp.Data.GraphQL.Server.AspNetCore.Giraffe type Startup private () = - let rootFactory (ctx) : Root = Root(ctx) + let rootFactory (ctx) : Root = Root (ctx) - new (configuration: IConfiguration) as this = - Startup() then - this.Configuration <- configuration + new (configuration : IConfiguration) as this = + Startup () + then this.Configuration <- configuration - member _.ConfigureServices(services: IServiceCollection) = + member _.ConfigureServices (services : IServiceCollection) = services .AddGiraffe() - .AddGraphQLOptions( - Schema.executor, - rootFactory, - "/ws" - ) + .AddGraphQLOptions (Schema.executor, rootFactory, "/ws") |> ignore - member _.Configure(app: IApplicationBuilder, env: IHostEnvironment, applicationLifetime : IHostApplicationLifetime, loggerFactory : ILoggerFactory) = + member _.Configure + ( + app : IApplicationBuilder, + env : IHostEnvironment + ) = let errorHandler (ex : Exception) (log : ILogger) = - log.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.") + log.LogError (EventId (), ex, "An unhandled exception has occurred while executing the request.") clearResponse >=> setStatusCode 500 - if env.IsDevelopment() then - app.UseGraphQLPlayground("/playground") |> ignore - app.UseGraphQLVoyager("/voyager") |> ignore - app.UseRouting() |> ignore - app.UseEndpoints(fun endpoints -> endpoints.MapBananaCakePop(PathString "/cakePop") |> ignore) |> ignore + if env.IsDevelopment () then + app.UseGraphQLPlayground ("/playground") |> ignore + app.UseGraphQLVoyager ("/voyager") |> ignore + app.UseRouting () |> ignore + app.UseEndpoints (fun endpoints -> endpoints.MapBananaCakePop (PathString "/cakePop") |> ignore) + |> ignore app .UseGiraffeErrorHandler(errorHandler) .UseWebSockets() .UseWebSocketsForGraphQL() - .UseGiraffe - (HttpHandlers.handleGraphQLWithResponseInterception - applicationLifetime.ApplicationStopping - (loggerFactory.CreateLogger("FSharp.Data.GraphQL.Server.AspNetCore.HttpHandlers.handleGraphQL")) - (setHttpHeader "Request-Type" "Classic")) + .UseGiraffe ( + HttpHandlers.graphQL + >=> (setHttpHeader "Request-Type" "Classic") + ) member val Configuration : IConfiguration = null with get, set diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj index 89c8efa99..1b4aed803 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj @@ -12,13 +12,18 @@ + + - + + + + diff --git a/samples/star-wars-api/GQLRequest.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GQLRequest.fs similarity index 94% rename from samples/star-wars-api/GQLRequest.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/GQLRequest.fs index 5b247e293..87aa9589d 100644 --- a/samples/star-wars-api/GQLRequest.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GQLRequest.fs @@ -1,4 +1,4 @@ -namespace FSharp.Data.GraphQL.Samples.StarWarsApi +namespace FSharp.Data.GraphQL.Server.AspNetCore open System.Collections.Immutable open System.Text.Json diff --git a/samples/star-wars-api/Ast.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/Ast.fs similarity index 95% rename from samples/star-wars-api/Ast.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/Ast.fs index 5f0abeaff..202496773 100644 --- a/samples/star-wars-api/Ast.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/Ast.fs @@ -1,4 +1,4 @@ -module FSharp.Data.GraphQL.Samples.StarWarsApi.Ast +module FSharp.Data.GraphQL.Server.AspNetCore.Ast open System.Collections.Immutable open FSharp.Data.GraphQL diff --git a/samples/star-wars-api/HttpContext.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs similarity index 85% rename from samples/star-wars-api/HttpContext.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs index f40ea94a6..1f8e59460 100644 --- a/samples/star-wars-api/HttpContext.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs @@ -1,5 +1,5 @@ [] -module Microsoft.AspNetCore.Http.HttpContextExtensions +module FSharp.Data.GraphQL.Server.AspNetCore.HttpContextExtensions open System.Collections.Generic open System.Collections.Immutable @@ -7,10 +7,10 @@ open System.IO open System.Runtime.CompilerServices open System.Text.Json open Microsoft.AspNetCore.Http +open Microsoft.Extensions.DependencyInjection open FSharp.Core open FsToolkit.ErrorHandling -open Giraffe type HttpContext with @@ -26,14 +26,14 @@ type HttpContext with /// [] member ctx.TryBindJsonAsync<'T>(expectedJson) = taskResult { - let serializer = ctx.GetJsonSerializer() + let serializerOptions = ctx.RequestServices.GetRequiredService().SerializerOptions let request = ctx.Request try if not request.Body.CanSeek then request.EnableBuffering() - return! serializer.DeserializeAsync<'T> request.Body + return! JsonSerializer.DeserializeAsync<'T>(request.Body, serializerOptions, ctx.RequestAborted) with :? JsonException -> let body = request.Body body.Seek(0, SeekOrigin.Begin) |> ignore diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 63a253a64..2b0d96b8d 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -1,155 +1,268 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore.Giraffe -open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL -open Giraffe -open FSharp.Data.GraphQL.Server.AspNetCore -open FsToolkit.ErrorHandling -open Microsoft.AspNetCore.Http -open Microsoft.Extensions.DependencyInjection -open Microsoft.Extensions.Logging -open System.Collections.Generic +open System +open System.IO open System.Text.Json open System.Text.Json.Serialization -open System.Threading open System.Threading.Tasks +open Microsoft.AspNetCore.Http +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Logging + +open FsToolkit.ErrorHandling +open Giraffe + +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Ast +open FSharp.Data.GraphQL.Server.AspNetCore type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult module HttpHandlers = - open System.Collections.Immutable - - let private httpOk - (cancellationToken : CancellationToken) - (customHandler : HttpHandler) - (serializerOptions : JsonSerializerOptions) - payload - : HttpHandler = - setStatusCode 200 - >=> customHandler - >=> (setHttpHeader "Content-Type" "application/json") - >=> (fun _ ctx -> - JsonSerializer - .SerializeAsync(ctx.Response.Body, payload, options = serializerOptions, cancellationToken = cancellationToken) - .ContinueWith (fun _ -> Some ctx) // what about when serialization fails? Maybe it will never at this stage anyway... - ) - - let private prepareGenericErrors (errorMessages : string list) = - (NameValueLookup.ofList [ - "errors", - upcast - (errorMessages - |> List.map (fun msg -> NameValueLookup.ofList [ "message", upcast msg ])) - ]) - - /// HttpHandler for handling GraphQL requests with Giraffe. - /// This one is for specifying an interceptor when you need to - /// do a custom handling on the response. For example, when you want - /// to add custom headers. - let handleGraphQLWithResponseInterception<'Root> - (cancellationToken : CancellationToken) - (logger : ILogger) - (interceptor : HttpHandler) - (next : HttpFunc) - (ctx : HttpContext) - = - task { - let cancellationToken = - CancellationTokenSource - .CreateLinkedTokenSource(cancellationToken, ctx.RequestAborted) - .Token - if cancellationToken.IsCancellationRequested then - return (fun _ -> None) ctx - else - let options = ctx.RequestServices.GetRequiredService> () - let executor = options.SchemaExecutor - let rootFactory = options.RootFactory - let serializerOptions = options.SerializerOptions - let deserializeGraphQLRequest () = task { - try - let! deserialized = JsonSerializer.DeserializeAsync (ctx.Request.Body, serializerOptions) - return Ok deserialized - with :? GraphQLException as ex -> - logger.LogError (``exception`` = ex, message = "Error while deserializing request.") - return Result.Error [ $"%s{ex.Message}\n%s{ex.ToString ()}" ] - } - let applyPlanExecutionResult (result : GQLExecutionResult) = task { - let gqlResponse = - match result.Content with - | Direct (data, errs) -> GQLResponse.Direct (result.DocumentId, data, errs) - | RequestError (problemDetailsList) -> GQLResponse.RequestError (result.DocumentId, problemDetailsList) - | _ -> - GQLResponse.RequestError ( - result.DocumentId, - [ - GQLProblemDetails.Create ("subscriptions are not supported here (use the websocket endpoint instead).") - ] + let rec private moduleType = getModuleType <@ moduleType @> + + let ofTaskIResult ctx (taskRes: Task) : HttpFuncResult = task { + let! res = taskRes + do! res.ExecuteAsync(ctx) + return Some ctx + } + + let ofTaskIResult2 ctx (taskRes: Task>) : HttpFuncResult = + taskRes + |> TaskResult.defaultWith id + |> ofTaskIResult ctx + + /// Set CORS to allow external servers (React samples) to call this API + let setCorsHeaders : HttpHandler = + setHttpHeader "Access-Control-Allow-Origin" "*" + >=> setHttpHeader "Access-Control-Allow-Headers" "content-type" + + let private handleGraphQL<'Root> (next : HttpFunc) (ctx : HttpContext) = + let sp = ctx.RequestServices + + let logger = sp.CreateLogger moduleType + + let options = sp.GetRequiredService>() + + let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } = + + let serializeIdented value = + let jsonSerializerOptions = options.GetSerializerOptionsIdented() + JsonSerializer.Serialize(value, jsonSerializerOptions) + + match content with + | RequestError errs -> + logger.LogInformation( + $"Produced request error GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", + documentId, + metadata + ) + + GQLResponse.RequestError(documentId, errs) + | Direct(data, errs) -> + logger.LogInformation( + $"Produced direct GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", + documentId, + metadata + ) + + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace($"GraphQL response data:{Environment.NewLine}:{{data}}", serializeIdented data) + + GQLResponse.Direct(documentId, data, errs) + | Deferred(data, errs, deferred) -> + logger.LogInformation( + $"Produced deferred GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", + documentId, + metadata + ) + + if logger.IsEnabled LogLevel.Information then + deferred + |> Observable.add (function + | DeferredResult(data, path) -> + logger.LogInformation( + "Produced GraphQL deferred result for path: {path}", + path |> Seq.map string |> Seq.toArray |> Path.Join ) - return! httpOk cancellationToken interceptor serializerOptions gqlResponse next ctx - } - let handleDeserializedGraphQLRequest (graphqlRequest : GraphQLRequest) = task { - match graphqlRequest.Query with + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace( + $"GraphQL deferred data:{Environment.NewLine}{{data}}", + serializeIdented data + ) + | DeferredErrors(null, errors, path) -> + logger.LogInformation( + "Produced GraphQL deferred errors for path: {path}", + path |> Seq.map string |> Seq.toArray |> Path.Join + ) + + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace($"GraphQL deferred errors:{Environment.NewLine}{{errors}}", errors) + | DeferredErrors(data, errors, path) -> + logger.LogInformation( + "Produced GraphQL deferred result with errors for path: {path}", + path |> Seq.map string |> Seq.toArray |> Path.Join + ) + + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace( + $"GraphQL deferred errors:{Environment.NewLine}{{errors}}{Environment.NewLine}GraphQL deferred data:{Environment.NewLine}{{data}}", + errors, + serializeIdented data + )) + + GQLResponse.Direct(documentId, data, errs) + | Stream stream -> + logger.LogInformation( + $"Produced stream GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", + documentId, + metadata + ) + + if logger.IsEnabled LogLevel.Information then + stream + |> Observable.add (function + | SubscriptionResult data -> + logger.LogInformation("Produced GraphQL subscription result") + + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace( + $"GraphQL subscription data:{Environment.NewLine}{{data}}", + serializeIdented data + ) + | SubscriptionErrors(null, errors) -> + logger.LogInformation("Produced GraphQL subscription errors") + + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace($"GraphQL subscription errors:{Environment.NewLine}{{errors}}", errors) + | SubscriptionErrors(data, errors) -> + logger.LogInformation("Produced GraphQL subscription result with errors") + + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace( + $"GraphQL subscription errors:{Environment.NewLine}{{errors}}{Environment.NewLine}GraphQL deferred data:{Environment.NewLine}{{data}}", + errors, + serializeIdented data + )) + + GQLResponse.Stream documentId + + /// Checks if the request contains a body + let checkIfHasBody (request: HttpRequest) = task { + if request.Body.CanSeek then + return (request.Body.Length > 0L) + else + request.EnableBuffering() + let body = request.Body + let buffer = Array.zeroCreate 1 + let! bytesRead = body.ReadAsync(buffer, 0, 1) + body.Seek(0, SeekOrigin.Begin) |> ignore + return bytesRead > 0 + } + + /// Check if the request is an introspection query + /// by first checking on such properties as `GET` method or `empty request body` + /// and lastly by parsing document AST for introspection operation definition. + /// + /// Result of check of + let checkOperationType (ctx: HttpContext) = taskResult { + + let checkAnonymousFieldsOnly (ctx: HttpContext) = taskResult { + let! gqlRequest = ctx.TryBindJsonAsync(GQLRequestContent.expectedJSON) + let! ast = Parser.parseOrIResult ctx.Request.Path.Value gqlRequest.Query + let operationName = gqlRequest.OperationName |> Skippable.toOption + + let createParsedContent() = { + Query = gqlRequest.Query + Ast = ast + OperationName = gqlRequest.OperationName + Variables = gqlRequest.Variables + } + if ast.IsEmpty then + logger.LogTrace( + "Request is not GET, but 'query' field is an empty string. Must be an introspection query" + ) + return IntrospectionQuery <| ValueNone + else + match Ast.findOperationByName operationName ast with | None -> - let! result = - executor.AsyncExecute (IntrospectionQuery.Definition) - |> Async.StartAsTask - if logger.IsEnabled (LogLevel.Debug) then - logger.LogDebug ($"Result metadata: %A{result.Metadata}") + logger.LogTrace "Document has no operation" + return IntrospectionQuery <| ValueNone + | Some op -> + if not (op.OperationType = Ast.Query) then + logger.LogTrace "Document operation is not of type Query" + return createParsedContent () |> OperationQuery else - () - return! result |> applyPlanExecutionResult - | Some queryAsStr -> - let graphQLQueryDecodingResult = - queryAsStr - |> GraphQLQueryDecoding.decodeGraphQLQuery - serializerOptions - executor - graphqlRequest.OperationName - graphqlRequest.Variables - match graphQLQueryDecodingResult with - | Result.Error struct (docId, probDetails) -> - return! httpOk cancellationToken interceptor serializerOptions (GQLResponse.RequestError (docId, probDetails)) next ctx - | Ok query -> - if logger.IsEnabled (LogLevel.Debug) then - logger.LogDebug ($"Received query: %A{query}") - else - () - let root = rootFactory (ctx) - let! result = - let variables = - ImmutableDictionary.CreateRange ( - query.Variables - |> Map.map (fun _ value -> JsonSerializer.SerializeToElement (value)) - ) - executor.AsyncExecute (query.ExecutionPlan, data = root, variables = variables) - |> Async.StartAsTask - if logger.IsEnabled (LogLevel.Debug) then - logger.LogDebug ($"Result metadata: %A{result.Metadata}") + let hasNonMetaFields = + Ast.containsFieldsBeyond + Ast.metaTypeFields + (fun x -> + logger.LogTrace($"Operation Selection in Field with name: {{fieldName}}", x.Name)) + (fun _ -> logger.LogTrace "Operation Selection is non-Field type") + op + + if hasNonMetaFields then + return createParsedContent() |> OperationQuery else - () - return! result |> applyPlanExecutionResult - } - if ctx.Request.Headers.ContentLength.GetValueOrDefault (0) = 0 then - let! result = - executor.AsyncExecute (IntrospectionQuery.Definition) - |> Async.StartAsTask - if logger.IsEnabled (LogLevel.Debug) then - logger.LogDebug ($"Result metadata: %A{result.Metadata}") - else - () - return! result |> applyPlanExecutionResult + return IntrospectionQuery <| ValueSome ast + } + + let request = ctx.Request + + if HttpMethods.Get = request.Method then + logger.LogTrace("Request is GET. Must be an introspection query") + return IntrospectionQuery <| ValueNone + else + let! hasBody = checkIfHasBody request + + if not hasBody then + logger.LogTrace("Request is not GET, but has no body. Must be an introspection query") + return IntrospectionQuery <| ValueNone else - match! deserializeGraphQLRequest () with - | Result.Error errMsgs -> - let probDetails = - errMsgs - |> List.map (fun msg -> GQLProblemDetails.Create (msg, Skip)) - return! httpOk cancellationToken interceptor serializerOptions (GQLResponse.RequestError (-1, probDetails)) next ctx - | Ok graphqlRequest -> return! handleDeserializedGraphQLRequest graphqlRequest + return! checkAnonymousFieldsOnly ctx + } + + /// Execute default or custom introspection query + let executeIntrospectionQuery (executor: Executor<_>) (ast: Ast.Document voption) = task { + let! result = + match ast with + | ValueNone -> executor.AsyncExecute IntrospectionQuery.Definition + | ValueSome ast -> executor.AsyncExecute ast + + let response = result |> toResponse + return Results.Ok response + } + + /// Execute the operation for given request + let executeOperation (executor: Executor<_>) content = task { + let operationName = content.OperationName |> Skippable.filter (not << isNull) |> Skippable.toOption + let variables = content.Variables |> Skippable.filter (not << isNull) |> Skippable.toOption + + operationName + |> Option.iter (fun on -> logger.LogTrace("GraphQL operation name: '{operationName}'", on)) + + logger.LogTrace($"Executing GraphQL query:{Environment.NewLine}{{query}}", content.Query) + + variables + |> Option.iter (fun v -> logger.LogTrace($"GraphQL variables:{Environment.NewLine}{{variables}}", v)) + + let root = options.RootFactory ctx + + let! result = executor.AsyncExecute(content.Ast, root, ?variables = variables, ?operationName = operationName) + + let response = result |> toResponse + return Results.Ok response + } + + taskResult { + let executor = options.SchemaExecutor + ctx.Response.Headers.Add("Request-Type", "Classic") // For integration testing purposes + match! checkOperationType ctx with + | IntrospectionQuery optionalAstDocument -> return! executeIntrospectionQuery executor optionalAstDocument + | OperationQuery content -> return! executeOperation executor content } + |> ofTaskIResult2 ctx - /// HttpHandler for handling GraphQL requests with Giraffe - let handleGraphQL<'Root> (cancellationToken : CancellationToken) (logger : ILogger) (next : HttpFunc) (ctx : HttpContext) = - handleGraphQLWithResponseInterception<'Root> cancellationToken logger id next ctx + let graphQL<'Root> : HttpHandler = setCorsHeaders >=> choose [ POST; GET ] >=> handleGraphQL<'Root> diff --git a/samples/star-wars-api/Parser.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/Parser.fs similarity index 83% rename from samples/star-wars-api/Parser.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/Parser.fs index d22399d03..b338a45cd 100644 --- a/samples/star-wars-api/Parser.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/Parser.fs @@ -1,4 +1,4 @@ -module FSharp.Data.GraphQL.Samples.StarWarsApi.Parser +module FSharp.Data.GraphQL.Server.AspNetCore.Parser open Microsoft.AspNetCore.Http open FSharp.Data.GraphQL diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs index bb49bbff2..773414dc5 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs @@ -14,6 +14,11 @@ type GraphQLTransportWSOptions = { CustomPingHandler : PingHandler option } +type IGraphQLOptions = + abstract member SerializerOptions : JsonSerializerOptions + abstract member WebsocketOptions : GraphQLTransportWSOptions + abstract member GetSerializerOptionsIdented : unit -> JsonSerializerOptions + type GraphQLOptions<'Root> = { SchemaExecutor : Executor<'Root> RootFactory : HttpContext -> 'Root @@ -25,3 +30,8 @@ type GraphQLOptions<'Root> = { let options = JsonSerializerOptions (options.SerializerOptions) options.WriteIndented <- true options + + interface IGraphQLOptions with + member this.SerializerOptions = this.SerializerOptions + member this.WebsocketOptions = this.WebsocketOptions + member this.GetSerializerOptionsIdented () = this.GetSerializerOptionsIdented () diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index fc16491d1..751fcb72b 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -1,18 +1,19 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore -open FSharp.Data.GraphQL -open Microsoft.AspNetCore.Http open System open System.Collections.Generic open System.Net.WebSockets open System.Text.Json +open System.Text.Json.Serialization open System.Threading open System.Threading.Tasks -open FSharp.Data.GraphQL.Execution -open FsToolkit.ErrorHandling +open Microsoft.AspNetCore.Http open Microsoft.Extensions.Hosting open Microsoft.Extensions.Logging -open System.Collections.Immutable +open FsToolkit.ErrorHandling + +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Execution type GraphQLWebSocketMiddleware<'Root> ( @@ -87,7 +88,7 @@ type GraphQLWebSocketMiddleware<'Root> return Some result } - let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) = task { + let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) : Task = task { if not (socket.State = WebSocketState.Open) then logger.LogTrace ("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) else @@ -104,7 +105,7 @@ type GraphQLWebSocketMiddleware<'Root> let addClientSubscription (id : SubscriptionId) - (howToSendDataOnNext : SubscriptionId -> 'ResponseContent -> Task) + (howToSendDataOnNext : SubscriptionId -> 'ResponseContent -> Task) (subscriptions : SubscriptionsDict, socket : WebSocket, streamSource : IObservable<'ResponseContent>, @@ -112,14 +113,11 @@ type GraphQLWebSocketMiddleware<'Root> = let observer = new Reactive.AnonymousObserver<'ResponseContent> ( - onNext = (fun theOutput -> theOutput |> howToSendDataOnNext id |> Task.WaitAll), + onNext = (fun theOutput -> (howToSendDataOnNext id theOutput).Wait()), onError = (fun ex -> logger.LogError (ex, "Error on subscription with id='{id}'", id)), onCompleted = (fun () -> - Complete id - |> sendMessageViaSocket jsonSerializerOptions (socket) - |> Async.AwaitTask - |> Async.RunSynchronously + (sendMessageViaSocket jsonSerializerOptions socket (Complete id)).Wait () subscriptions |> GraphQLSubscriptionsManagement.removeSubscription (id)) ) @@ -181,13 +179,13 @@ type GraphQLWebSocketMiddleware<'Root> printfn "Deferred response errors: %s" (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) Task.FromResult (()) - let sendDeferredResultDelayedBy (cancToken : CancellationToken) (ms : int) id deferredResult = task { + let sendDeferredResultDelayedBy (cancToken : CancellationToken) (ms : int) id deferredResult : Task = task { do! Async.StartAsTask (Async.Sleep ms, cancellationToken = cancToken) do! deferredResult |> sendDeferredResponseOutput id } let sendQueryOutputDelayedBy = sendDeferredResultDelayedBy cancellationToken - let applyPlanExecutionResult (id : SubscriptionId) (socket) (executionResult : GQLExecutionResult) = task { + let applyPlanExecutionResult (id : SubscriptionId) (socket) (executionResult : GQLExecutionResult) : Task = task { match executionResult with | Stream observableOutput -> (subscriptions, socket, observableOutput, serializerOptions) @@ -261,14 +259,9 @@ type GraphQLWebSocketMiddleware<'Root> CancellationToken.None ) else - let variables = - ImmutableDictionary.CreateRange ( - query.Variables - |> Map.map (fun _ value -> value :?> JsonElement) - ) + let variables = query.Variables |> Skippable.toOption let! planExecutionResult = - executor.AsyncExecute (query.ExecutionPlan, root (httpContext), variables) - |> Async.StartAsTask + executor.AsyncExecute (query.Query, root (httpContext), ?variables = variables) do! planExecutionResult |> applyPlanExecutionResult id socket | ClientComplete id -> "ClientComplete" |> logMsgWithIdReceived id diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs new file mode 100644 index 000000000..bfe415ae8 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs @@ -0,0 +1,49 @@ +namespace FSharp.Data.GraphQL.Server.AspNetCore + +open System +open System.Text + + +[] +module Helpers = + + let tee f x = + f x + x + + +[] +module StringHelpers = + + let utf8String (bytes : byte seq) = + bytes + |> Seq.filter (fun i -> i > 0uy) + |> Array.ofSeq + |> Encoding.UTF8.GetString + + let utf8Bytes (str : string) = str |> Encoding.UTF8.GetBytes + + let isNullOrWhiteSpace (str : string) = String.IsNullOrWhiteSpace (str) + + +[] +module LoggingHelpers = + + open Microsoft.Extensions.DependencyInjection + open Microsoft.Extensions.Logging + + type IServiceProvider with + member serviceProvider.CreateLogger (``type`` : Type) = + let loggerFactory = serviceProvider.GetRequiredService() + loggerFactory.CreateLogger(``type``) + + +[] +module ReflectionHelpers = + + open Microsoft.FSharp.Quotations.Patterns + + let getModuleType = function + | PropertyGet (_, propertyInfo, _) -> propertyInfo.DeclaringType + | _ -> failwith "Expression is no property." + diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs index 12fd5fe90..140a900c1 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs @@ -1,7 +1,6 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL.Types open System open System.Text.Json open System.Collections.Generic @@ -11,13 +10,6 @@ type SubscriptionUnsubscriber = IDisposable type OnUnsubscribeAction = SubscriptionId -> unit type SubscriptionsDict = IDictionary -type GraphQLRequest = { - OperationName : string option - Query : string option - Variables : JsonDocument option - Extensions : string option -} - type RawMessage = { Id : string option; Type : string; Payload : JsonDocument option } type ServerRawPayload = @@ -27,13 +19,11 @@ type ServerRawPayload = type RawServerMessage = { Id : string option; Type : string; Payload : ServerRawPayload option } -type GraphQLQuery = { ExecutionPlan : ExecutionPlan; Variables : Map } - type ClientMessage = | ConnectionInit of payload : JsonDocument option | ClientPing of payload : JsonDocument option | ClientPong of payload : JsonDocument option - | Subscribe of id : string * query : GraphQLQuery + | Subscribe of id : string * query : GQLRequestContent | ClientComplete of id : string type ClientMessageProtocolFailure = InvalidMessage of code : int * explanation : string @@ -47,6 +37,7 @@ type ServerMessage = | Complete of id : string module CustomWebSocketStatus = + let invalidMessage = 4400 let unauthorized = 4401 let connectionTimeout = 4408 diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs deleted file mode 100644 index b99b39ba0..000000000 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/GraphQLQueryDecoding.fs +++ /dev/null @@ -1,84 +0,0 @@ -namespace FSharp.Data.GraphQL.Server.AspNetCore - -module GraphQLQueryDecoding = - open FSharp.Data.GraphQL - open System - open System.Text.Json - open System.Text.Json.Serialization - open FsToolkit.ErrorHandling - - let genericErrorContentForDoc (docId : int) (message : string) = struct (docId, [ GQLProblemDetails.Create (message, Skip) ]) - - let genericFinalErrorForDoc (docId : int) (message : string) = Result.Error (genericErrorContentForDoc docId message) - - let genericFinalError message = message |> genericFinalErrorForDoc -1 - - let private resolveVariables - (serializerOptions : JsonSerializerOptions) - (expectedVariables : Types.VarDef list) - (variableValuesObj : JsonDocument) - = - result { - try - try - if (not (variableValuesObj.RootElement.ValueKind.Equals (JsonValueKind.Object))) then - let offendingValueKind = variableValuesObj.RootElement.ValueKind - return! Result.Error ($"\"variables\" must be an object, but here it is \"%A{offendingValueKind}\" instead") - else - let providedVariableValues = - variableValuesObj.RootElement.EnumerateObject () - |> List.ofSeq - return! - Ok ( - expectedVariables - |> List.choose (fun expectedVariable -> - providedVariableValues - |> List.tryFind (fun x -> x.Name = expectedVariable.Name) - |> Option.map (fun providedValue -> - let boxedValue = - if providedValue.Value.ValueKind.Equals (JsonValueKind.Null) then - null :> obj - elif providedValue.Value.ValueKind.Equals (JsonValueKind.String) then - providedValue.Value.GetString () :> obj - else - JsonSerializer.Deserialize (providedValue.Value, serializerOptions) :> obj - (expectedVariable.Name, boxedValue))) - |> Map.ofList - ) - with - | :? JsonException as ex -> return! Result.Error (ex.Message) - | :? GraphQLException as ex -> return! Result.Error (ex.Message) - | ex -> - printfn "%s" (ex.ToString ()) - return! Result.Error ("Something unexpected happened during the parsing of this request.") - finally - variableValuesObj.Dispose () - } - - let decodeGraphQLQuery - (serializerOptions : JsonSerializerOptions) - (executor : Executor<'a>) - (operationName : string option) - (variables : JsonDocument option) - (query : string) - = - let executionPlanResult = result { - try - match operationName with - | Some operationName -> return! executor.CreateExecutionPlan (query, operationName = operationName) - | None -> return! executor.CreateExecutionPlan (query) - with - | :? JsonException as ex -> return! genericFinalError (ex.Message) - | :? GraphQLException as ex -> return! genericFinalError (ex.Message) - } - - executionPlanResult - |> Result.bind (fun executionPlan -> - match variables with - | None -> Ok <| (executionPlan, Map.empty) // it's none of our business here if some variables are expected. If that's the case, execution of the ExecutionPlan will take care of that later (and issue an error). - | Some variableValuesObj -> - variableValuesObj - |> resolveVariables serializerOptions executionPlan.Variables - |> Result.map (fun variableValsObj -> (executionPlan, variableValsObj)) - |> Result.mapError (genericErrorContentForDoc executionPlan.DocumentId)) - |> Result.map (fun (executionPlan, variables) -> { ExecutionPlan = executionPlan; Variables = variables }) diff --git a/samples/star-wars-api/JSON.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs similarity index 61% rename from samples/star-wars-api/JSON.fs rename to src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs index 9c8403182..2d949f2ac 100644 --- a/samples/star-wars-api/JSON.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs @@ -1,5 +1,5 @@ [] -module FSharp.Data.GraphQL.Samples.StarWarsApi.Json +module FSharp.Data.GraphQL.Server.AspNetCore.Json open System.Text.Json open System.Text.Json.Serialization @@ -11,11 +11,19 @@ let configureSerializerOptions (jsonFSharpOptions: JsonFSharpOptions) (additiona options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase options.PropertyNameCaseInsensitive <- true let converters = options.Converters - converters.Add (JsonStringEnumConverter ()) + converters.Add (new JsonStringEnumConverter ()) //converters.Add (JsonSerializerOptionsState (options)) // Dahomey.Json additionalConverters |> Seq.iter converters.Add jsonFSharpOptions.AddToJsonSerializerOptions options +let configureWSSerializerOptions (jsonFSharpOptions: JsonFSharpOptions) (additionalConverters: JsonConverter seq) (options : JsonSerializerOptions) = + let additionalConverters = seq { + yield new ClientMessageConverter () :> JsonConverter + yield new RawServerMessageConverter () + yield! additionalConverters + } + configureSerializerOptions jsonFSharpOptions additionalConverters options + let defaultJsonFSharpOptions = JsonFSharpOptions( JsonUnionEncoding.InternalTag @@ -29,10 +37,16 @@ let defaultJsonFSharpOptions = allowOverride = true) let configureDefaultSerializerOptions = configureSerializerOptions defaultJsonFSharpOptions +let configureDefaultWSSerializerOptions = configureWSSerializerOptions defaultJsonFSharpOptions let getSerializerOptions (additionalConverters: JsonConverter seq) = let options = JsonSerializerOptions () options |> configureDefaultSerializerOptions additionalConverters options -let serializerOptions = getSerializerOptions Seq.empty +let getWSSerializerOptions (additionalConverters: JsonConverter seq) = + let options = JsonSerializerOptions () + options |> configureDefaultWSSerializerOptions additionalConverters + options + +let serializerOptions = getWSSerializerOptions Seq.empty diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index 84797060a..6cf6540cd 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -6,7 +6,7 @@ open System.Text.Json open System.Text.Json.Serialization [] -type ClientMessageConverter<'Root> (executor : Executor<'Root>) = +type ClientMessageConverter () = inherit JsonConverter () let raiseInvalidMsg explanation = raise <| InvalidMessageException explanation @@ -45,29 +45,19 @@ type ClientMessageConverter<'Root> (executor : Executor<'Root>) = let requireSubscribePayload (serializerOptions : JsonSerializerOptions) - (executor : Executor<'a>) (payload : JsonDocument option) - : Result = + : Result = match payload with | None -> invalidMsg <| "payload is required for this message, but none was present." | Some p -> - let rawSubsPayload = JsonSerializer.Deserialize (p, serializerOptions) - match rawSubsPayload with - | None -> + try + JsonSerializer.Deserialize(p, serializerOptions) |> Ok + with + | :? JsonException as ex -> invalidMsg - <| "payload is required for this message, but none was present." - | Some subscribePayload -> - match subscribePayload.Query with - | None -> - invalidMsg - <| "there was no query in the client's subscribe message!" - | Some query -> - query - |> GraphQLQueryDecoding.decodeGraphQLQuery serializerOptions executor subscribePayload.OperationName subscribePayload.Variables - |> Result.mapError (fun errMsg -> InvalidMessage (CustomWebSocketStatus.invalidMessage, errMsg |> errMsgToStr)) - + <| $"invalid payload received: {ex.Message}." let readRawMessage (reader : byref, options : JsonSerializerOptions) : RawMessage = if not (reader.TokenType.Equals (JsonTokenType.StartObject)) then @@ -104,7 +94,7 @@ type ClientMessageConverter<'Root> (executor : Executor<'Root>) = |> requireId |> Result.bind (fun id -> raw.Payload - |> requireSubscribePayload options executor + |> requireSubscribePayload options |> Result.map (fun payload -> (id, payload))) |> Result.map Subscribe |> unpackRopResult @@ -141,31 +131,3 @@ type RawServerMessageConverter () = | CustomResponse jsonDocument -> jsonDocument.WriteTo (writer) writer.WriteEndObject () - - -module JsonConverterUtils = - - [] - let UnionTag = "kind" - - let private defaultJsonFSharpOptions = - JsonFSharpOptions ( - JsonUnionEncoding.InternalTag - ||| JsonUnionEncoding.AllowUnorderedTag - ||| JsonUnionEncoding.NamedFields - ||| JsonUnionEncoding.UnwrapSingleCaseUnions - ||| JsonUnionEncoding.UnwrapRecordCases - ||| JsonUnionEncoding.UnwrapOption - ||| JsonUnionEncoding.UnwrapFieldlessTags, - UnionTag, - allowOverride = true - ) - let configureSerializer (executor : Executor<'Root>) (jsonSerializerOptions : JsonSerializerOptions) = - jsonSerializerOptions.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase - jsonSerializerOptions.PropertyNameCaseInsensitive <- true - jsonSerializerOptions.Converters.Add (new JsonStringEnumConverter ()) - jsonSerializerOptions.Converters.Add (new ClientMessageConverter<'Root> (executor)) - jsonSerializerOptions.Converters.Add (new RawServerMessageConverter ()) - jsonSerializerOptions - |> defaultJsonFSharpOptions.AddToJsonSerializerOptions - jsonSerializerOptions diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index 4a430b1cc..9c3e98f6f 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -1,21 +1,20 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore -open FSharp.Data.GraphQL -open Microsoft.AspNetCore.Builder -open Microsoft.Extensions.DependencyInjection +open System +open System.Runtime.InteropServices open System.Runtime.CompilerServices -open System.Text.Json +open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http +open Microsoft.Extensions.DependencyInjection +open FSharp.Data.GraphQL -[] -type ServiceCollectionExtensions () = +[] +module ServiceCollectionExtensions = - static let createStandardOptions executor rootFactory endpointUrl = { + let createStandardOptions executor rootFactory endpointUrl = { SchemaExecutor = executor RootFactory = rootFactory - SerializerOptions = - JsonSerializerOptions (IgnoreNullValues = true) - |> JsonConverterUtils.configureSerializer executor + SerializerOptions = Json.serializerOptions WebsocketOptions = { EndpointUrl = endpointUrl ConnectionInitTimeoutInMs = 3000 @@ -23,29 +22,42 @@ type ServiceCollectionExtensions () = } } - [] - static member AddGraphQLOptions<'Root> - ( - this : IServiceCollection, - executor : Executor<'Root>, - rootFactory : HttpContext -> 'Root, - endpointUrl : string - ) = - this.AddSingleton> (createStandardOptions executor rootFactory endpointUrl) - - [] - static member AddGraphQLOptionsWith<'Root> - ( - this : IServiceCollection, - executor : Executor<'Root>, - rootFactory : HttpContext -> 'Root, - endpointUrl : string, - extraConfiguration : GraphQLOptions<'Root> -> GraphQLOptions<'Root> - ) = - this.AddSingleton> ( - createStandardOptions executor rootFactory endpointUrl - |> extraConfiguration - ) - - [] - static member UseWebSocketsForGraphQL<'Root> (this : IApplicationBuilder) = this.UseMiddleware> () + // See https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions + type MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions + // See https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.json.jsonoptions + type HttpClientJsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions + + type IServiceCollection with + + [] + member services.AddGraphQLOptions<'Root> + ( + executor : Executor<'Root>, + rootFactory : HttpContext -> 'Root, + endpointUrl : string, + [] configure : Func, GraphQLOptions<'Root>> + ) = + let options = + let options = createStandardOptions executor rootFactory endpointUrl + match configure with + | null -> options + | _ -> configure.Invoke options + services + // We need this for output serialization purposes as we use + // Surprisingly minimal APIs use Microsoft.AspNetCore.Http.Json.JsonOptions + // Use if you want to return HTTP responses using minmal APIs IResult interface + .Configure( + Action(fun o -> + Json.configureDefaultSerializerOptions Seq.empty o.SerializerOptions + ) + ) + .AddSingleton>(options) + .AddSingleton>(fun sp -> sp.GetRequiredService>()) + +[] +module ApplicationBuilderExtensions = + + type IApplicationBuilder with + + [] + member builder.UseWebSocketsForGraphQL<'Root> () = builder.UseMiddleware> () From f482a989b7f3b9800a60f2a2806b72d8244ae2da Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 17:23:56 +0400 Subject: [PATCH 078/100] Added GraphQL playgounds to chat-app --- .../server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj | 3 +++ samples/chat-app/server/Program.fs | 6 ++++++ samples/chat-app/server/Properties/launchSettings.json | 3 +++ 3 files changed, 12 insertions(+) diff --git a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj index e44d44b6e..f1f7eef89 100644 --- a/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj +++ b/samples/chat-app/server/FSharp.Data.GraphQL.Samples.ChatApp.fsproj @@ -7,6 +7,9 @@ + + + diff --git a/samples/chat-app/server/Program.fs b/samples/chat-app/server/Program.fs index 7b1af3c8c..5e77bb9cd 100644 --- a/samples/chat-app/server/Program.fs +++ b/samples/chat-app/server/Program.fs @@ -26,6 +26,12 @@ let main args = let app = builder.Build () + if app.Environment.IsDevelopment () then + app.UseGraphQLPlayground ("/playground") |> ignore + app.UseGraphQLVoyager ("/voyager") |> ignore + app.UseRouting () |> ignore + app.UseEndpoints (fun endpoints -> endpoints.MapBananaCakePop (PathString "/cakePop") |> ignore) + |> ignore app .UseGiraffeErrorHandler(errorHandler) diff --git a/samples/chat-app/server/Properties/launchSettings.json b/samples/chat-app/server/Properties/launchSettings.json index 40fef14da..90117f03b 100644 --- a/samples/chat-app/server/Properties/launchSettings.json +++ b/samples/chat-app/server/Properties/launchSettings.json @@ -12,6 +12,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, + "launchUrl": "cakePop", "applicationUrl": "http://localhost:5092", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -21,6 +22,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, + "launchUrl": "cakePop", "applicationUrl": "https://localhost:7122;http://localhost:5092", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -29,6 +31,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "cakePop", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } From 41e5ec35d361d05d6c6f3518b74f983741756db3 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 17:56:06 +0400 Subject: [PATCH 079/100] Fixed constant like values naming style --- .../GraphQLWebsocketMiddleware.fs | 8 ++++---- .../Messages.fs | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 751fcb72b..01f1eb834 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -237,7 +237,7 @@ type GraphQLWebSocketMiddleware<'Root> "ConnectionInit" |> logMsgReceivedWithOptionalPayload p do! socket.CloseAsync ( - enum CustomWebSocketStatus.tooManyInitializationRequests, + enum CustomWebSocketStatus.TooManyInitializationRequests, "too many initialization requests", CancellationToken.None ) @@ -254,7 +254,7 @@ type GraphQLWebSocketMiddleware<'Root> if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then do! socket.CloseAsync ( - enum CustomWebSocketStatus.subscriberAlreadyExists, + enum CustomWebSocketStatus.SubscriberAlreadyExists, $"Subscriber for %s{id} already exists", CancellationToken.None ) @@ -292,7 +292,7 @@ type GraphQLWebSocketMiddleware<'Root> let detonationRegistration = timerTokenSource.Token.Register (fun _ -> socket - |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.connectionTimeout, "Connection initialization timeout") + |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.ConnectionTimeout, "Connection initialization timeout") |> Task.WaitAll) let! connectionInitSucceeded = @@ -311,7 +311,7 @@ type GraphQLWebSocketMiddleware<'Root> | Ok (Some (Subscribe _)) -> do! socket - |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.unauthorized, "Unauthorized") + |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.Unauthorized, "Unauthorized") return false | Result.Error (InvalidMessage (code, explanation)) -> do! diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs index 140a900c1..9c29369ac 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs @@ -1,9 +1,9 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore -open FSharp.Data.GraphQL.Execution open System -open System.Text.Json open System.Collections.Generic +open System.Text.Json +open FSharp.Data.GraphQL.Execution type SubscriptionId = string type SubscriptionUnsubscriber = IDisposable @@ -38,8 +38,8 @@ type ServerMessage = module CustomWebSocketStatus = - let invalidMessage = 4400 - let unauthorized = 4401 - let connectionTimeout = 4408 - let subscriberAlreadyExists = 4409 - let tooManyInitializationRequests = 4429 + let InvalidMessage = 4400 + let Unauthorized = 4401 + let ConnectionTimeout = 4408 + let SubscriberAlreadyExists = 4409 + let TooManyInitializationRequests = 4429 From 379c35473b746acfee86fa0fd2ae20f3a0c788df Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 17:56:35 +0400 Subject: [PATCH 080/100] Fixed tests --- .../GraphQLSubscriptionsManagement.fs | 2 ++ .../GraphQLWebsocketMiddleware.fs | 1 + .../Messages.fs | 3 +- .../Serialization/JsonConverters.fs | 4 ++- .../AspNetCore/InvalidMessageTests.fs | 6 ++-- .../AspNetCore/SerializationTests.fs | 33 +++++-------------- .../ExecutionTests.fs | 2 +- .../FSharp.Data.GraphQL.Tests.fsproj | 1 - tests/FSharp.Data.GraphQL.Tests/Helpers.fs | 3 +- .../IntrospectionTests.fs | 2 +- .../Variables and Inputs/InputComplexTests.fs | 2 +- .../Variables and Inputs/InputEnumTests.fs | 2 +- .../Variables and Inputs/InputListTests.fs | 2 +- .../Variables and Inputs/InputNestedTests.fs | 2 +- .../InputNullableStringTests.fs | 2 +- .../InputObjectValidatorTests.fs | 2 +- .../Variables and Inputs/InputRecordTests.fs | 2 +- .../OptionalsNormalizationTests.fs | 2 +- 18 files changed, 30 insertions(+), 43 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs index 3ebd2246d..9e9e7ab84 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLSubscriptionsManagement.fs @@ -1,5 +1,7 @@ module internal FSharp.Data.GraphQL.Server.AspNetCore.GraphQLSubscriptionsManagement +open FSharp.Data.GraphQL.Server.AspNetCore.WebSockets + let addSubscription (id : SubscriptionId, unsubscriber : SubscriptionUnsubscriber, onUnsubscribe : OnUnsubscribeAction) (subscriptions : SubscriptionsDict) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 01f1eb834..5cc7239b2 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -14,6 +14,7 @@ open FsToolkit.ErrorHandling open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Execution +open FSharp.Data.GraphQL.Server.AspNetCore.WebSockets type GraphQLWebSocketMiddleware<'Root> ( diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs index 9c29369ac..82a649f50 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs @@ -1,9 +1,10 @@ -namespace FSharp.Data.GraphQL.Server.AspNetCore +namespace FSharp.Data.GraphQL.Server.AspNetCore.WebSockets open System open System.Collections.Generic open System.Text.Json open FSharp.Data.GraphQL.Execution +open FSharp.Data.GraphQL.Server.AspNetCore type SubscriptionId = string type SubscriptionUnsubscriber = IDisposable diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index 6cf6540cd..79db624d7 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -1,10 +1,12 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore -open FSharp.Data.GraphQL open System open System.Text.Json open System.Text.Json.Serialization +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Server.AspNetCore.WebSockets + [] type ClientMessageConverter () = inherit JsonConverter () diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs index 10456be60..3e2c60774 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs @@ -1,14 +1,14 @@ module FSharp.Data.GraphQL.Tests.AspNetCore.InvalidMessageTests -open FSharp.Data.GraphQL.Tests.AspNetCore -open FSharp.Data.GraphQL.Server.AspNetCore open System.Text.Json open Xunit +open FSharp.Data.GraphQL.Server.AspNetCore +open FSharp.Data.GraphQL.Server.AspNetCore.WebSockets let toClientMessage (theInput : string) = let serializerOptions = new JsonSerializerOptions () serializerOptions.PropertyNameCaseInsensitive <- true - serializerOptions.Converters.Add (new ClientMessageConverter (TestSchema.executor)) + serializerOptions.Converters.Add (new ClientMessageConverter()) serializerOptions.Converters.Add (new RawServerMessageConverter ()) JsonSerializer.Deserialize (theInput, serializerOptions) diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs index 9ebf2d1de..7efd051a6 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs @@ -1,14 +1,16 @@ module FSharp.Data.GraphQL.Tests.AspNetCore.SerializationTests +open Xunit +open System.Text.Json open FSharp.Data.GraphQL.Ast open FSharp.Data.GraphQL.Server.AspNetCore -open System.Text.Json -open Xunit +open FSharp.Data.GraphQL.Server.AspNetCore.WebSockets +open System.Text.Json.Serialization let getStdSerializerOptions () = let serializerOptions = new JsonSerializerOptions () serializerOptions.PropertyNameCaseInsensitive <- true - serializerOptions.Converters.Add (new ClientMessageConverter (TestSchema.executor)) + serializerOptions.Converters.Add (new ClientMessageConverter ()) serializerOptions.Converters.Add (new RawServerMessageConverter ()) serializerOptions @@ -114,26 +116,7 @@ let ``Deserializes client subscription correctly`` () = match result with | Subscribe (id, payload) -> Assert.Equal ("b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6", id) - Assert.Equal (1, payload.ExecutionPlan.Operation.SelectionSet.Length) - let watchMoonSelection = payload.ExecutionPlan.Operation.SelectionSet |> List.head - match watchMoonSelection with - | Field watchMoonField -> - Assert.Equal ("watchMoon", watchMoonField.Name) - Assert.Equal (1, watchMoonField.Arguments.Length) - let watchMoonFieldArg = watchMoonField.Arguments |> List.head - Assert.Equal ("id", watchMoonFieldArg.Name) - match watchMoonFieldArg.Value with - | StringValue theValue -> Assert.Equal ("1", theValue) - | other -> Assert.Fail ($"expected arg to be a StringValue, but it was: %A{other}") - Assert.Equal (3, watchMoonField.SelectionSet.Length) - match watchMoonField.SelectionSet.[0] with - | Field firstField -> Assert.Equal ("id", firstField.Name) - | other -> Assert.Fail ($"expected field to be a Field, but it was: %A{other}") - match watchMoonField.SelectionSet.[1] with - | Field secondField -> Assert.Equal ("name", secondField.Name) - | other -> Assert.Fail ($"expected field to be a Field, but it was: %A{other}") - match watchMoonField.SelectionSet.[2] with - | Field thirdField -> Assert.Equal ("isMoon", thirdField.Name) - | other -> Assert.Fail ($"expected field to be a Field, but it was: %A{other}") - | somethingElse -> Assert.Fail ($"expected it to be a field, but it was: %A{somethingElse}") + Assert.Equal ("subscription { watchMoon(id: \"1\") { id name isMoon } }", payload.Query) + Assert.Equal (Skip, payload.OperationName) + Assert.Equal (Skip, payload.Variables) | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") diff --git a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs index dac698e39..7afa11e95 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs @@ -16,7 +16,7 @@ open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore type TestSubject = { a: string 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 f47abf98c..cc744167e 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -24,7 +24,6 @@ - diff --git a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs index a50199789..7d304ebd1 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs @@ -74,7 +74,7 @@ let greaterThanOrEqual expected actual = open System.Text.Json open FSharp.Data.GraphQL.Types -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore let stringifyArg name (ctx : ResolveFieldContext) () = let arg = ctx.TryArg name |> Option.toObj @@ -167,7 +167,6 @@ module Observer = new TestObserver<'T>(sub, onReceive) open System.Runtime.CompilerServices -open FSharp.Data.GraphQL.Types [] type ExecutorExtensions = diff --git a/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs b/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs index 6828af3ed..195e24a11 100644 --- a/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs @@ -7,7 +7,7 @@ open Xunit open System open System.Text.Json open System.Text.Json.Serialization -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore #nowarn "25" 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 5262ee3ab..ecebe84e4 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputComplexTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputComplexTests.fs @@ -16,7 +16,7 @@ open FSharp.Data.GraphQL.Ast open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore open ErrorHelpers let TestComplexScalar = diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputEnumTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputEnumTests.fs index 333644a31..5229b190b 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputEnumTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputEnumTests.fs @@ -14,7 +14,7 @@ open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore let stringifyArg name (ctx : ResolveFieldContext) () = let arg = ctx.TryArg name |> Option.toObj diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputListTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputListTests.fs index e475fd407..d5b8de7eb 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputListTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputListTests.fs @@ -14,7 +14,7 @@ open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore open ErrorHelpers let stringifyArg name (ctx : ResolveFieldContext) () = 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 a024c78c9..2ba48355d 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs @@ -13,7 +13,7 @@ open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore let InputArrayOf (innerDef : #TypeDef<'Val>) : ListOfDef<'Val, 'Val array> = ListOf innerDef diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNullableStringTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNullableStringTests.fs index 1bace1186..9ae2386b7 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNullableStringTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNullableStringTests.fs @@ -14,7 +14,7 @@ open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore open ErrorHelpers let stringifyArg name (ctx : ResolveFieldContext) () = diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputObjectValidatorTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputObjectValidatorTests.fs index 255fba572..54a850115 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputObjectValidatorTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputObjectValidatorTests.fs @@ -15,7 +15,7 @@ open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Execution open FSharp.Data.GraphQL.Validation open FSharp.Data.GraphQL.Validation.ValidationResult -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore open ErrorHelpers type InputRecord = { Country : string; ZipCode : string; City : string } diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordTests.fs index af9f028a0..cf38f0ac7 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputRecordTests.fs @@ -11,7 +11,7 @@ open System.Text.Json open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Parser -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore type InputRecord = { a : string; b : string; c : string } diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs index 34717d2ad..1fb88fc2e 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs @@ -13,7 +13,7 @@ open System.Text.Json open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Parser -open FSharp.Data.GraphQL.Samples.StarWarsApi +open FSharp.Data.GraphQL.Server.AspNetCore module Phantom = From fc506709bdef57004893ce5037238bfda54bbcdf Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 18:15:25 +0400 Subject: [PATCH 081/100] Fixed `FSharp.Data.GraphQL.Server.AspNetCore` tests Used default `JsonSerializerSettings` Fixed error messages --- .../Serialization/JsonConverters.fs | 18 ++++++------- .../AspNetCore/InvalidMessageTests.fs | 27 +++++++++---------- .../AspNetCore/SerializationTests.fs | 16 ++--------- 3 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index 79db624d7..cd2390945 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -36,14 +36,14 @@ type ClientMessageConverter () = getOptionalString (&reader) else raiseInvalidMsg - <| $"was expecting a value for property \"%s{propertyName}\"" + <| $"Was expecting a value for property \"%s{propertyName}\"" let requireId (raw : RawMessage) : Result = match raw.Id with | Some s -> Ok s | None -> invalidMsg - <| "property \"id\" is required for this message but was not present." + <| "Property \"id\" is required for this message but was not present." let requireSubscribePayload (serializerOptions : JsonSerializerOptions) @@ -52,14 +52,14 @@ type ClientMessageConverter () = match payload with | None -> invalidMsg - <| "payload is required for this message, but none was present." + <| "Payload is required for this message, but none was present." | Some p -> try JsonSerializer.Deserialize(p, serializerOptions) |> Ok with | :? JsonException as ex -> invalidMsg - <| $"invalid payload received: {ex.Message}." + <| $"Invalid payload received: {ex.Message}." let readRawMessage (reader : byref, options : JsonSerializerOptions) : RawMessage = if not (reader.TokenType.Equals (JsonTokenType.StartObject)) then @@ -74,10 +74,10 @@ type ClientMessageConverter () = | "id" -> id <- readPropertyValueAsAString "id" &reader | "type" -> theType <- readPropertyValueAsAString "type" &reader | "payload" -> payload <- Some <| JsonDocument.ParseValue (&reader) - | other -> raiseInvalidMsg <| $"unknown property \"%s{other}\"" + | other -> raiseInvalidMsg <| $"Unknown property \"%s{other}\"" match theType with - | None -> raiseInvalidMsg "property \"type\" is missing" + | None -> raiseInvalidMsg "Property \"type\" is missing" | Some msgType -> { Id = id; Type = msgType; Payload = payload } override __.Read (reader : byref, typeToConvert : Type, options : JsonSerializerOptions) : ClientMessage = @@ -102,18 +102,18 @@ type ClientMessageConverter () = |> unpackRopResult | other -> raiseInvalidMsg - <| $"invalid type \"%s{other}\" specified by client." + <| $"Invalid type \"%s{other}\" specified by client." override __.Write (writer : Utf8JsonWriter, value : ClientMessage, options : JsonSerializerOptions) = - failwith "serializing a WebSocketClientMessage is not supported (yet(?))" + raise (NotSupportedException "Serializing a WebSocketClientMessage is not supported (yet(?))") [] type RawServerMessageConverter () = inherit JsonConverter () override __.Read (reader : byref, typeToConvert : Type, options : JsonSerializerOptions) : RawServerMessage = - failwith "deserializing a RawServerMessage is not supported (yet(?))" + raise (NotSupportedException "deserializing a RawServerMessage is not supported (yet(?))") override __.Write (writer : Utf8JsonWriter, value : RawServerMessage, options : JsonSerializerOptions) = writer.WriteStartObject () diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs index 3e2c60774..3ec862f88 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs @@ -6,10 +6,7 @@ open FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL.Server.AspNetCore.WebSockets let toClientMessage (theInput : string) = - let serializerOptions = new JsonSerializerOptions () - serializerOptions.PropertyNameCaseInsensitive <- true - serializerOptions.Converters.Add (new ClientMessageConverter()) - serializerOptions.Converters.Add (new RawServerMessageConverter ()) + let serializerOptions = Json.serializerOptions JsonSerializer.Deserialize (theInput, serializerOptions) let willResultInInvalidMessage expectedExplanation input = @@ -23,7 +20,7 @@ let willResultInInvalidMessage expectedExplanation input = let willResultInJsonException input = try input |> toClientMessage |> ignore - Assert.Fail ("expected that a JsonException would have already been thrown at this point") + Assert.Fail ("Expected that a JsonException would have already been thrown at this point") with :? JsonException as ex -> Assert.True (true) @@ -33,7 +30,7 @@ let ``Unknown message type will result in invalid message`` () = "type": "connection_start" } """ - |> willResultInInvalidMessage "invalid type \"connection_start\" specified by client." + |> willResultInInvalidMessage "Invalid type \"connection_start\" specified by client." [] let ``Type not specified will result in invalid message`` () = @@ -41,7 +38,7 @@ let ``Type not specified will result in invalid message`` () = "payload": "hello, let us connect" } """ - |> willResultInInvalidMessage "property \"type\" is missing" + |> willResultInInvalidMessage "Property \"type\" is missing" [] let ``No payload in subscribe message will result in invalid message`` () = @@ -50,7 +47,7 @@ let ``No payload in subscribe message will result in invalid message`` () = "id": "b5d4d2ff-d262-4882-a7b9-d6aec5e4faa6" } """ - |> willResultInInvalidMessage "payload is required for this message, but none was present." + |> willResultInInvalidMessage "Payload is required for this message, but none was present." [] let ``Null payload json in subscribe message will result in invalid message`` () = @@ -60,7 +57,7 @@ let ``Null payload json in subscribe message will result in invalid message`` () "payload": null } """ - |> willResultInInvalidMessage "payload is required for this message, but none was present." + |> willResultInInvalidMessage "Invalid payload received: Failed to parse type FSharp.Data.GraphQL.Server.AspNetCore.GQLRequestContent: expected JSON object, found Null." [] let ``Payload type of number in subscribe message will result in invalid message`` () = @@ -71,7 +68,7 @@ let ``Payload type of number in subscribe message will result in invalid message } """ |> willResultInInvalidMessage - "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AspNetCore.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 2." + "Invalid payload received: Failed to parse type FSharp.Data.GraphQL.Server.AspNetCore.GQLRequestContent: expected JSON object, found Number." [] let ``No id in subscribe message will result in invalid message`` () = @@ -82,7 +79,7 @@ let ``No id in subscribe message will result in invalid message`` () = } } """ - |> willResultInInvalidMessage "property \"id\" is required for this message but was not present." + |> willResultInInvalidMessage "Property \"id\" is required for this message but was not present." [] let ``String payload wrongly used in subscribe will result in invalid message`` () = @@ -93,7 +90,7 @@ let ``String payload wrongly used in subscribe will result in invalid message`` } """ |> willResultInInvalidMessage - "The JSON value could not be converted to FSharp.Data.GraphQL.Server.AspNetCore.GraphQLRequest. Path: $ | LineNumber: 0 | BytePositionInLine: 79." + "Invalid payload received: Failed to parse type FSharp.Data.GraphQL.Server.AspNetCore.GQLRequestContent: expected JSON object, found String." [] let ``Id is incorrectly a number in a subscribe message will result in JsonException`` () = @@ -117,7 +114,7 @@ let ``Typo in one of the messages root properties will result in invalid message } } """ - |> willResultInInvalidMessage "unknown property \"typo\"" + |> willResultInInvalidMessage "Unknown property \"typo\"" [] let ``Complete message without an id will result in invalid message`` () = @@ -125,7 +122,7 @@ let ``Complete message without an id will result in invalid message`` () = "type": "complete" } """ - |> willResultInInvalidMessage "property \"id\" is required for this message but was not present." + |> willResultInInvalidMessage "Property \"id\" is required for this message but was not present." [] let ``Complete message with a null id will result in invalid message`` () = @@ -134,4 +131,4 @@ let ``Complete message with a null id will result in invalid message`` () = "id": null } """ - |> willResultInInvalidMessage "property \"id\" is required for this message but was not present." + |> willResultInInvalidMessage "Property \"id\" is required for this message but was not present." diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs index 7efd051a6..db0406049 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs @@ -7,16 +7,11 @@ open FSharp.Data.GraphQL.Server.AspNetCore open FSharp.Data.GraphQL.Server.AspNetCore.WebSockets open System.Text.Json.Serialization -let getStdSerializerOptions () = - let serializerOptions = new JsonSerializerOptions () - serializerOptions.PropertyNameCaseInsensitive <- true - serializerOptions.Converters.Add (new ClientMessageConverter ()) - serializerOptions.Converters.Add (new RawServerMessageConverter ()) - serializerOptions +let serializerOptions = Json.serializerOptions [] let ``Deserializes ConnectionInit correctly`` () = - let serializerOptions = getStdSerializerOptions () + let input = "{\"type\":\"connection_init\"}" let result = JsonSerializer.Deserialize (input, serializerOptions) @@ -27,7 +22,6 @@ let ``Deserializes ConnectionInit correctly`` () = [] let ``Deserializes ConnectionInit with payload correctly`` () = - let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"connection_init\", \"payload\":\"hello\"}" @@ -39,7 +33,6 @@ let ``Deserializes ConnectionInit with payload correctly`` () = [] let ``Deserializes ClientPing correctly`` () = - let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"ping\"}" @@ -51,7 +44,6 @@ let ``Deserializes ClientPing correctly`` () = [] let ``Deserializes ClientPing with payload correctly`` () = - let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"ping\", \"payload\":\"ping!\"}" @@ -63,7 +55,6 @@ let ``Deserializes ClientPing with payload correctly`` () = [] let ``Deserializes ClientPong correctly`` () = - let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"pong\"}" @@ -75,7 +66,6 @@ let ``Deserializes ClientPong correctly`` () = [] let ``Deserializes ClientPong with payload correctly`` () = - let serializerOptions = getStdSerializerOptions () let input = "{\"type\":\"pong\", \"payload\": \"pong!\"}" @@ -87,7 +77,6 @@ let ``Deserializes ClientPong with payload correctly`` () = [] let ``Deserializes ClientComplete correctly`` () = - let serializerOptions = getStdSerializerOptions () let input = "{\"id\": \"65fca2b5-f149-4a70-a055-5123dea4628f\", \"type\":\"complete\"}" @@ -99,7 +88,6 @@ let ``Deserializes ClientComplete correctly`` () = [] let ``Deserializes client subscription correctly`` () = - let serializerOptions = getStdSerializerOptions () let input = """{ From e65e943f9dc4fa7dc58b5dacadb6e9d87f4c20dd Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 18:37:44 +0400 Subject: [PATCH 082/100] Fixed integration tests --- FSharp.Data.GraphQL.Integration.sln | 47 ++++++++++++------- ...ata.GraphQL.IntegrationTests.Server.fsproj | 5 -- .../Startup.fs | 6 +-- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/FSharp.Data.GraphQL.Integration.sln b/FSharp.Data.GraphQL.Integration.sln index b0a6acbe7..8b44b9d95 100644 --- a/FSharp.Data.GraphQL.Integration.sln +++ b/FSharp.Data.GraphQL.Integration.sln @@ -8,17 +8,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Packages.props = Packages.props EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BA7F22E2-D411-4229-826B-F55FF171D12A}" +EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.IntegrationTests.Server", "tests\FSharp.Data.GraphQL.IntegrationTests.Server\FSharp.Data.GraphQL.IntegrationTests.Server.fsproj", "{E6754A20-FA5E-4C76-AB1B-D35DF9526889}" EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.IntegrationTests", "tests\FSharp.Data.GraphQL.IntegrationTests\FSharp.Data.GraphQL.IntegrationTests.fsproj", "{09D910E6-94EF-46AF-94DF-10A9FEC837C0}" EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Server", "src\FSharp.Data.GraphQL.Server\FSharp.Data.GraphQL.Server.fsproj", "{CA16AC10-9FF2-4894-AC73-99FBD35BB8CC}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BDE03396-2ED6-4153-B94C-351BAB3F67BD}" EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Shared", "src\FSharp.Data.GraphQL.Shared\FSharp.Data.GraphQL.Shared.fsproj", "{237F9575-6E65-40DD-A77B-BA2882BD5646}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BDE03396-2ED6-4153-B94C-351BAB3F67BD}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Server", "src\FSharp.Data.GraphQL.Server\FSharp.Data.GraphQL.Server.fsproj", "{CA16AC10-9FF2-4894-AC73-99FBD35BB8CC}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BA7F22E2-D411-4229-826B-F55FF171D12A}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.GraphQL.Server.AspNetCore", "src\FSharp.Data.GraphQL.Server.AspNetCore\FSharp.Data.GraphQL.Server.AspNetCore.fsproj", "{9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -30,18 +32,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|x64.ActiveCfg = Debug|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|x64.Build.0 = Debug|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|x86.ActiveCfg = Debug|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|x86.Build.0 = Debug|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|Any CPU.Build.0 = Release|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|x64.ActiveCfg = Release|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|x64.Build.0 = Release|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|x86.ActiveCfg = Release|Any CPU - {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|x86.Build.0 = Release|Any CPU {E6754A20-FA5E-4C76-AB1B-D35DF9526889}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E6754A20-FA5E-4C76-AB1B-D35DF9526889}.Debug|Any CPU.Build.0 = Debug|Any CPU {E6754A20-FA5E-4C76-AB1B-D35DF9526889}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -54,6 +44,18 @@ Global {E6754A20-FA5E-4C76-AB1B-D35DF9526889}.Release|x64.Build.0 = Release|Any CPU {E6754A20-FA5E-4C76-AB1B-D35DF9526889}.Release|x86.ActiveCfg = Release|Any CPU {E6754A20-FA5E-4C76-AB1B-D35DF9526889}.Release|x86.Build.0 = Release|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|x64.ActiveCfg = Debug|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|x64.Build.0 = Debug|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|x86.ActiveCfg = Debug|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Debug|x86.Build.0 = Debug|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|Any CPU.Build.0 = Release|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|x64.ActiveCfg = Release|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|x64.Build.0 = Release|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|x86.ActiveCfg = Release|Any CPU + {09D910E6-94EF-46AF-94DF-10A9FEC837C0}.Release|x86.Build.0 = Release|Any CPU {CA16AC10-9FF2-4894-AC73-99FBD35BB8CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CA16AC10-9FF2-4894-AC73-99FBD35BB8CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA16AC10-9FF2-4894-AC73-99FBD35BB8CC}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -78,15 +80,28 @@ Global {237F9575-6E65-40DD-A77B-BA2882BD5646}.Release|x64.Build.0 = Release|Any CPU {237F9575-6E65-40DD-A77B-BA2882BD5646}.Release|x86.ActiveCfg = Release|Any CPU {237F9575-6E65-40DD-A77B-BA2882BD5646}.Release|x86.Build.0 = Release|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Debug|x64.Build.0 = Debug|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Debug|x86.Build.0 = Debug|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Release|Any CPU.Build.0 = Release|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Release|x64.ActiveCfg = Release|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Release|x64.Build.0 = Release|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Release|x86.ActiveCfg = Release|Any CPU + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {09D910E6-94EF-46AF-94DF-10A9FEC837C0} = {BA7F22E2-D411-4229-826B-F55FF171D12A} {E6754A20-FA5E-4C76-AB1B-D35DF9526889} = {BA7F22E2-D411-4229-826B-F55FF171D12A} + {09D910E6-94EF-46AF-94DF-10A9FEC837C0} = {BA7F22E2-D411-4229-826B-F55FF171D12A} {CA16AC10-9FF2-4894-AC73-99FBD35BB8CC} = {BDE03396-2ED6-4153-B94C-351BAB3F67BD} {237F9575-6E65-40DD-A77B-BA2882BD5646} = {BDE03396-2ED6-4153-B94C-351BAB3F67BD} + {9E795521-CC0E-4E9C-9DC1-66CFCC7A31C4} = {BDE03396-2ED6-4153-B94C-351BAB3F67BD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1B712506-56AA-424E-9DB7-47BCF3894516} diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj index 79c803a84..0c3c27d86 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj @@ -16,14 +16,9 @@ - - - - - diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs index c73a47ebd..3e7b9d5fe 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs @@ -43,10 +43,6 @@ type Startup private () = let loggerFactory = app.ApplicationServices.GetRequiredService() app .UseGiraffeErrorHandler(errorHandler) - .UseGiraffe - (HttpHandlers.handleGraphQLWithResponseInterception - applicationLifeTime.ApplicationStopping - (loggerFactory.CreateLogger("HttpHandlers.handleGraphQL")) - (setHttpHeader "Request-Type" "Classic")) + .UseGiraffe (HttpHandlers.graphQL >=> (setHttpHeader "Request-Type" "Classic")) member val Configuration : IConfiguration = null with get, set From 1281c27aa9e85a7dc894e32f5132dba26a5f8491 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 10 Mar 2024 18:42:50 +0400 Subject: [PATCH 083/100] Added missing `FSharp.Data.GraphQL.Server.AspNetCore` package reference to `Prepare template project for packing.ps1` script --- Prepare template project for packing.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Prepare template project for packing.ps1 b/Prepare template project for packing.ps1 index e3abcd8de..7f233c1e6 100644 --- a/Prepare template project for packing.ps1 +++ b/Prepare template project for packing.ps1 @@ -9,6 +9,7 @@ $version = $dirBuildTargets.SelectSingleNode("//PropertyGroup[@Label='NuGet']/Ve [xml]$fsharpPackages = @" + From a3ce3c68144cf672c06ef52496b1a2f21a5d369e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 12 Mar 2024 00:08:03 +0400 Subject: [PATCH 084/100] Set appropriate logging level --- .../Giraffe/HttpHandlers.fs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 2b0d96b8d..8e7ee3c58 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -53,7 +53,7 @@ module HttpHandlers = match content with | RequestError errs -> - logger.LogInformation( + logger.LogDebug( $"Produced request error GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", documentId, metadata @@ -61,7 +61,7 @@ module HttpHandlers = GQLResponse.RequestError(documentId, errs) | Direct(data, errs) -> - logger.LogInformation( + logger.LogDebug( $"Produced direct GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", documentId, metadata @@ -72,17 +72,17 @@ module HttpHandlers = GQLResponse.Direct(documentId, data, errs) | Deferred(data, errs, deferred) -> - logger.LogInformation( + logger.LogDebug( $"Produced deferred GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", documentId, metadata ) - if logger.IsEnabled LogLevel.Information then + if logger.IsEnabled LogLevel.Debug then deferred |> Observable.add (function | DeferredResult(data, path) -> - logger.LogInformation( + logger.LogDebug( "Produced GraphQL deferred result for path: {path}", path |> Seq.map string |> Seq.toArray |> Path.Join ) @@ -93,7 +93,7 @@ module HttpHandlers = serializeIdented data ) | DeferredErrors(null, errors, path) -> - logger.LogInformation( + logger.LogDebug( "Produced GraphQL deferred errors for path: {path}", path |> Seq.map string |> Seq.toArray |> Path.Join ) @@ -101,7 +101,7 @@ module HttpHandlers = if logger.IsEnabled LogLevel.Trace then logger.LogTrace($"GraphQL deferred errors:{Environment.NewLine}{{errors}}", errors) | DeferredErrors(data, errors, path) -> - logger.LogInformation( + logger.LogDebug( "Produced GraphQL deferred result with errors for path: {path}", path |> Seq.map string |> Seq.toArray |> Path.Join ) @@ -115,17 +115,17 @@ module HttpHandlers = GQLResponse.Direct(documentId, data, errs) | Stream stream -> - logger.LogInformation( + logger.LogDebug( $"Produced stream GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", documentId, metadata ) - if logger.IsEnabled LogLevel.Information then + if logger.IsEnabled LogLevel.Debug then stream |> Observable.add (function | SubscriptionResult data -> - logger.LogInformation("Produced GraphQL subscription result") + logger.LogDebug("Produced GraphQL subscription result") if logger.IsEnabled LogLevel.Trace then logger.LogTrace( @@ -133,12 +133,12 @@ module HttpHandlers = serializeIdented data ) | SubscriptionErrors(null, errors) -> - logger.LogInformation("Produced GraphQL subscription errors") + logger.LogDebug("Produced GraphQL subscription errors") if logger.IsEnabled LogLevel.Trace then logger.LogTrace($"GraphQL subscription errors:{Environment.NewLine}{{errors}}", errors) | SubscriptionErrors(data, errors) -> - logger.LogInformation("Produced GraphQL subscription result with errors") + logger.LogDebug("Produced GraphQL subscription result with errors") if logger.IsEnabled LogLevel.Trace then logger.LogTrace( @@ -166,7 +166,7 @@ module HttpHandlers = /// by first checking on such properties as `GET` method or `empty request body` /// and lastly by parsing document AST for introspection operation definition. /// - /// Result of check of + /// Result of check of let checkOperationType (ctx: HttpContext) = taskResult { let checkAnonymousFieldsOnly (ctx: HttpContext) = taskResult { From 8e4943dc65c76506892bff81ff0c4bf9450d3774 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 12 Mar 2024 00:13:13 +0400 Subject: [PATCH 085/100] Moved Giraffe request handlers out of the GraphQL handler --- samples/star-wars-api/Startup.fs | 7 +++++-- .../Giraffe/HttpHandlers.fs | 8 +------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/samples/star-wars-api/Startup.fs b/samples/star-wars-api/Startup.fs index c6b2625f7..f47b66040 100644 --- a/samples/star-wars-api/Startup.fs +++ b/samples/star-wars-api/Startup.fs @@ -46,8 +46,11 @@ type Startup private () = .UseWebSockets() .UseWebSocketsForGraphQL() .UseGiraffe ( - HttpHandlers.graphQL - >=> (setHttpHeader "Request-Type" "Classic") + // Set CORS to allow external servers (React samples) to call this API + setHttpHeader "Access-Control-Allow-Origin" "*" + >=> setHttpHeader "Access-Control-Allow-Headers" "content-type" + >=> (setHttpHeader "Request-Type" "Classic") // For integration testing purposes + >=> HttpHandlers.graphQL ) member val Configuration : IConfiguration = null with get, set diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 8e7ee3c58..2654c0727 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -33,11 +33,6 @@ module HttpHandlers = |> TaskResult.defaultWith id |> ofTaskIResult ctx - /// Set CORS to allow external servers (React samples) to call this API - let setCorsHeaders : HttpHandler = - setHttpHeader "Access-Control-Allow-Origin" "*" - >=> setHttpHeader "Access-Control-Allow-Headers" "content-type" - let private handleGraphQL<'Root> (next : HttpFunc) (ctx : HttpContext) = let sp = ctx.RequestServices @@ -258,11 +253,10 @@ module HttpHandlers = taskResult { let executor = options.SchemaExecutor - ctx.Response.Headers.Add("Request-Type", "Classic") // For integration testing purposes match! checkOperationType ctx with | IntrospectionQuery optionalAstDocument -> return! executeIntrospectionQuery executor optionalAstDocument | OperationQuery content -> return! executeOperation executor content } |> ofTaskIResult2 ctx - let graphQL<'Root> : HttpHandler = setCorsHeaders >=> choose [ POST; GET ] >=> handleGraphQL<'Root> + let graphQL<'Root> : HttpHandler = choose [ POST; GET ] >=> handleGraphQL<'Root> From ee93b7f86989cb2328a3d1ff657d4c34f3104df3 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 12 Mar 2024 01:09:13 +0400 Subject: [PATCH 086/100] Fixed integration tests --- tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs index 3e7b9d5fe..f61c83cb9 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs @@ -43,6 +43,8 @@ type Startup private () = let loggerFactory = app.ApplicationServices.GetRequiredService() app .UseGiraffeErrorHandler(errorHandler) - .UseGiraffe (HttpHandlers.graphQL >=> (setHttpHeader "Request-Type" "Classic")) + .UseGiraffe ( + (setHttpHeader "Request-Type" "Classic") + >=> HttpHandlers.graphQL) member val Configuration : IConfiguration = null with get, set From a0c464d1ad02547c4db52bd1179965c0b4bee7f3 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 12 Mar 2024 01:09:46 +0400 Subject: [PATCH 087/100] Implemented request cancellation passing inside GraphQL execution --- .../Giraffe/HttpHandlers.fs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 2654c0727..e8cd6c317 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -245,18 +245,25 @@ module HttpHandlers = let root = options.RootFactory ctx - let! result = executor.AsyncExecute(content.Ast, root, ?variables = variables, ?operationName = operationName) + let! result = + Async.StartAsTask( + executor.AsyncExecute(content.Ast, root, ?variables = variables, ?operationName = operationName), + cancellationToken = ctx.RequestAborted + ) let response = result |> toResponse return Results.Ok response } - taskResult { - let executor = options.SchemaExecutor - match! checkOperationType ctx with - | IntrospectionQuery optionalAstDocument -> return! executeIntrospectionQuery executor optionalAstDocument - | OperationQuery content -> return! executeOperation executor content - } - |> ofTaskIResult2 ctx + if ctx.RequestAborted.IsCancellationRequested then + Task.FromResult None + else + taskResult { + let executor = options.SchemaExecutor + match! checkOperationType ctx with + | IntrospectionQuery optionalAstDocument -> return! executeIntrospectionQuery executor optionalAstDocument + | OperationQuery content -> return! executeOperation executor content + } + |> ofTaskIResult2 ctx let graphQL<'Root> : HttpHandler = choose [ POST; GET ] >=> handleGraphQL<'Root> From a6ee3e2907e1c8e51aa528ca6fd8890a56639036 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 17 Mar 2024 23:03:21 +0400 Subject: [PATCH 088/100] Set GraphQL request body error logging to the warning level and moved to the end --- .../Giraffe/HttpHandlers.fs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index e8cd6c317..f8e1370cf 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -47,14 +47,6 @@ module HttpHandlers = JsonSerializer.Serialize(value, jsonSerializerOptions) match content with - | RequestError errs -> - logger.LogDebug( - $"Produced request error GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", - documentId, - metadata - ) - - GQLResponse.RequestError(documentId, errs) | Direct(data, errs) -> logger.LogDebug( $"Produced direct GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", @@ -143,6 +135,14 @@ module HttpHandlers = )) GQLResponse.Stream documentId + | RequestError errs -> + logger.LogWarning( + $"Produced request error GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", + documentId, + metadata + ) + + GQLResponse.RequestError(documentId, errs) /// Checks if the request contains a body let checkIfHasBody (request: HttpRequest) = task { From 38fed3cb8161979508583e0bb82744cc1bb64ade Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 17 Mar 2024 23:45:45 +0400 Subject: [PATCH 089/100] Migrated to ValueOption, IOptions<> and simplified WebSocket handling logic --- .../Giraffe/HttpContext.fs | 3 +- .../Giraffe/HttpHandlers.fs | 11 ++- .../GraphQLOptions.fs | 11 +-- .../GraphQLWebsocketMiddleware.fs | 98 ++++++++----------- .../Messages.fs | 12 +-- .../Serialization/JsonConverters.fs | 34 +++---- .../StartupExtensions.fs | 17 +++- .../AspNetCore/SerializationTests.fs | 6 +- 8 files changed, 90 insertions(+), 102 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs index 1f8e59460..9a5492301 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpContext.fs @@ -8,6 +8,7 @@ open System.Runtime.CompilerServices open System.Text.Json open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Options open FSharp.Core open FsToolkit.ErrorHandling @@ -26,7 +27,7 @@ type HttpContext with /// [] member ctx.TryBindJsonAsync<'T>(expectedJson) = taskResult { - let serializerOptions = ctx.RequestServices.GetRequiredService().SerializerOptions + let serializerOptions = ctx.RequestServices.GetRequiredService>().Value.SerializerOptions let request = ctx.Request try diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index f8e1370cf..0ceafc718 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -8,6 +8,7 @@ open System.Threading.Tasks open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Logging +open Microsoft.Extensions.Options open FsToolkit.ErrorHandling open Giraffe @@ -20,6 +21,8 @@ type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult module HttpHandlers = + let [] internal IdentedOptionsName = "Idented" + let rec private moduleType = getModuleType <@ moduleType @> let ofTaskIResult ctx (taskRes: Task) : HttpFuncResult = task { @@ -38,12 +41,12 @@ module HttpHandlers = let logger = sp.CreateLogger moduleType - let options = sp.GetRequiredService>() + let options = sp.GetRequiredService>>() let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } = let serializeIdented value = - let jsonSerializerOptions = options.GetSerializerOptionsIdented() + let jsonSerializerOptions = options.Get(IdentedOptionsName).SerializerOptions JsonSerializer.Serialize(value, jsonSerializerOptions) match content with @@ -243,7 +246,7 @@ module HttpHandlers = variables |> Option.iter (fun v -> logger.LogTrace($"GraphQL variables:{Environment.NewLine}{{variables}}", v)) - let root = options.RootFactory ctx + let root = options.CurrentValue.RootFactory ctx let! result = Async.StartAsTask( @@ -259,7 +262,7 @@ module HttpHandlers = Task.FromResult None else taskResult { - let executor = options.SchemaExecutor + let executor = options.CurrentValue.SchemaExecutor match! checkOperationType ctx with | IntrospectionQuery optionalAstDocument -> return! executeIntrospectionQuery executor optionalAstDocument | OperationQuery content -> return! executeOperation executor content diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs index 773414dc5..537db5fdc 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLOptions.fs @@ -6,18 +6,17 @@ open System.Text.Json open System.Threading.Tasks open Microsoft.AspNetCore.Http -type PingHandler = IServiceProvider -> JsonDocument option -> Task +type PingHandler = IServiceProvider -> JsonDocument voption -> Task type GraphQLTransportWSOptions = { EndpointUrl : string ConnectionInitTimeoutInMs : int - CustomPingHandler : PingHandler option + CustomPingHandler : PingHandler voption } type IGraphQLOptions = abstract member SerializerOptions : JsonSerializerOptions abstract member WebsocketOptions : GraphQLTransportWSOptions - abstract member GetSerializerOptionsIdented : unit -> JsonSerializerOptions type GraphQLOptions<'Root> = { SchemaExecutor : Executor<'Root> @@ -26,12 +25,6 @@ type GraphQLOptions<'Root> = { WebsocketOptions : GraphQLTransportWSOptions } with - member options.GetSerializerOptionsIdented () = - let options = JsonSerializerOptions (options.SerializerOptions) - options.WriteIndented <- true - options - interface IGraphQLOptions with member this.SerializerOptions = this.SerializerOptions member this.WebsocketOptions = this.WebsocketOptions - member this.GetSerializerOptionsIdented () = this.GetSerializerOptionsIdented () diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 5cc7239b2..3c6a9f834 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -10,6 +10,7 @@ open System.Threading.Tasks open Microsoft.AspNetCore.Http open Microsoft.Extensions.Hosting open Microsoft.Extensions.Logging +open Microsoft.Extensions.Options open FsToolkit.ErrorHandling open FSharp.Data.GraphQL @@ -22,18 +23,24 @@ type GraphQLWebSocketMiddleware<'Root> applicationLifetime : IHostApplicationLifetime, serviceProvider : IServiceProvider, logger : ILogger>, - options : GraphQLOptions<'Root> + options : IOptions> ) = + let options = options.Value + let serializerOptions = options.SerializerOptions + let pingHandler = options.WebsocketOptions.CustomPingHandler + let endpointUrl = PathString options.WebsocketOptions.EndpointUrl + let connectionInitTimeout = options.WebsocketOptions.ConnectionInitTimeoutInMs + let serializeServerMessage (jsonSerializerOptions : JsonSerializerOptions) (serverMessage : ServerMessage) = task { let raw = match serverMessage with - | ConnectionAck -> { Id = None; Type = "connection_ack"; Payload = None } - | ServerPing -> { Id = None; Type = "ping"; Payload = None } - | ServerPong p -> { Id = None; Type = "pong"; Payload = p |> Option.map CustomResponse } - | Next (id, payload) -> { Id = Some id; Type = "next"; Payload = Some <| ExecutionResult payload } - | Complete id -> { Id = Some id; Type = "complete"; Payload = None } - | Error (id, errMsgs) -> { Id = Some id; Type = "error"; Payload = Some <| ErrorMessages errMsgs } + | ConnectionAck -> { Id = ValueNone; Type = "connection_ack"; Payload = ValueNone } + | ServerPing -> { Id = ValueNone; Type = "ping"; Payload = ValueNone } + | ServerPong p -> { Id = ValueNone; Type = "pong"; Payload = p |> ValueOption.map CustomResponse } + | Next (id, payload) -> { Id = ValueSome id; Type = "next"; Payload = ValueSome <| ExecutionResult payload } + | Complete id -> { Id = ValueSome id; Type = "complete"; Payload = ValueNone } + | Error (id, errMsgs) -> { Id = ValueSome id; Type = "error"; Payload = ValueSome <| ErrorMessages errMsgs } return JsonSerializer.Serialize (raw, jsonSerializerOptions) } @@ -83,10 +90,10 @@ type GraphQLWebSocketMiddleware<'Root> |> Array.ofSeq |> System.Text.Encoding.UTF8.GetString if String.IsNullOrWhiteSpace message then - return None + return ValueNone else let! result = message |> deserializeClientMessage serializerOptions - return Some result + return ValueSome result } let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) : Task = task { @@ -137,15 +144,7 @@ type GraphQLWebSocketMiddleware<'Root> let tryToGracefullyCloseSocketWithDefaultBehavior = tryToGracefullyCloseSocket (WebSocketCloseStatus.NormalClosure, "Normal Closure") - let handleMessages - (cancellationToken : CancellationToken) - (httpContext : HttpContext) - (serializerOptions : JsonSerializerOptions) - (executor : Executor<'Root>) - (root : HttpContext -> 'Root) - (pingHandler : PingHandler option) - (socket : WebSocket) - = + let handleMessages (cancellationToken : CancellationToken) (httpContext : HttpContext) (socket : WebSocket) : Task = let subscriptions = new Dictionary () // ----------> // Helpers --> @@ -204,8 +203,8 @@ type GraphQLWebSocketMiddleware<'Root> let getStrAddendumOfOptionalPayload optionalPayload = optionalPayload - |> Option.map (fun payloadStr -> $" with payload: %A{payloadStr}") - |> Option.defaultWith (fun () -> "") + |> ValueOption.map (fun payloadStr -> $" with payload: %A{payloadStr}") + |> ValueOption.defaultWith (fun () -> "") let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) = logger.LogTrace ("{message}{messageaddendum}", msgAsStr, (optionalPayload |> getStrAddendumOfOptionalPayload)) @@ -226,13 +225,13 @@ type GraphQLWebSocketMiddleware<'Root> let! receivedMessage = rcv () match receivedMessage with | Result.Error failureMsgs -> - "InvalidMessage" |> logMsgReceivedWithOptionalPayload None + "InvalidMessage" |> logMsgReceivedWithOptionalPayload ValueNone match failureMsgs with | InvalidMessage (code, explanation) -> do! socket.CloseAsync (enum code, explanation, CancellationToken.None) | Ok maybeMsg -> match maybeMsg with - | None -> logger.LogTrace ("Websocket socket received empty message! (socket state = {socketstate})", socket.State) - | Some msg -> + | ValueNone -> logger.LogTrace ("Websocket socket received empty message! (socket state = {socketstate})", socket.State) + | ValueSome msg -> match msg with | ConnectionInit p -> "ConnectionInit" |> logMsgReceivedWithOptionalPayload p @@ -245,10 +244,10 @@ type GraphQLWebSocketMiddleware<'Root> | ClientPing p -> "ClientPing" |> logMsgReceivedWithOptionalPayload p match pingHandler with - | Some func -> + | ValueSome func -> let! customP = p |> func serviceProvider do! ServerPong customP |> sendMsg - | None -> do! ServerPong p |> sendMsg + | ValueNone -> do! ServerPong p |> sendMsg | ClientPong p -> "ClientPong" |> logMsgReceivedWithOptionalPayload p | Subscribe (id, query) -> "Subscribe" |> logMsgWithIdReceived id @@ -262,7 +261,8 @@ type GraphQLWebSocketMiddleware<'Root> else let variables = query.Variables |> Skippable.toOption let! planExecutionResult = - executor.AsyncExecute (query.Query, root (httpContext), ?variables = variables) + let root = options.RootFactory httpContext + options.SchemaExecutor.AsyncExecute (query.Query, root, ?variables = variables) do! planExecutionResult |> applyPlanExecutionResult id socket | ClientComplete id -> "ClientComplete" |> logMsgWithIdReceived id @@ -282,14 +282,10 @@ type GraphQLWebSocketMiddleware<'Root> // <-- Main // <-------- - let waitForConnectionInitAndRespondToClient - (serializerOptions : JsonSerializerOptions) - (connectionInitTimeoutInMs : int) - (socket : WebSocket) - : TaskResult = - taskResult { + let waitForConnectionInitAndRespondToClient (socket : WebSocket) : TaskResult = + task { let timerTokenSource = new CancellationTokenSource () - timerTokenSource.CancelAfter (connectionInitTimeoutInMs) + timerTokenSource.CancelAfter connectionInitTimeout let detonationRegistration = timerTokenSource.Token.Register (fun _ -> socket @@ -302,14 +298,14 @@ type GraphQLWebSocketMiddleware<'Root> logger.LogDebug ("Waiting for ConnectionInit...") let! receivedMessage = receiveMessageViaSocket (CancellationToken.None) serializerOptions socket match receivedMessage with - | Ok (Some (ConnectionInit _)) -> + | Ok (ValueSome (ConnectionInit _)) -> logger.LogDebug ("Valid connection_init received! Responding with ACK!") detonationRegistration.Unregister () |> ignore do! ConnectionAck |> sendMessageViaSocket serializerOptions socket return true - | Ok (Some (Subscribe _)) -> + | Ok (ValueSome (Subscribe _)) -> do! socket |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.Unauthorized, "Unauthorized") @@ -327,46 +323,30 @@ type GraphQLWebSocketMiddleware<'Root> ) if (not timerTokenSource.Token.IsCancellationRequested) then if connectionInitSucceeded then - return () + return Ok () else - return! - Result.Error - <| "ConnectionInit failed (not because of timeout)" + return Result.Error ("ConnectionInit failed (not because of timeout)") else - return! Result.Error <| "ConnectionInit timeout" + return Result.Error <| "ConnectionInit timeout" } member __.InvokeAsync (ctx : HttpContext) = task { - if not (ctx.Request.Path = PathString (options.WebsocketOptions.EndpointUrl)) then + if not (ctx.Request.Path = endpointUrl) then do! next.Invoke (ctx) else if ctx.WebSockets.IsWebSocketRequest then use! socket = ctx.WebSockets.AcceptWebSocketAsync ("graphql-transport-ws") let! connectionInitResult = - socket - |> waitForConnectionInitAndRespondToClient options.SerializerOptions options.WebsocketOptions.ConnectionInitTimeoutInMs + socket |> waitForConnectionInitAndRespondToClient match connectionInitResult with - | Result.Error errMsg -> logger.LogWarning ("{warningmsg}", ($"%A{errMsg}")) + | Result.Error errMsg -> logger.LogWarning ("{warningmsg}", errMsg) | Ok _ -> let longRunningCancellationToken = (CancellationTokenSource .CreateLinkedTokenSource(ctx.RequestAborted, applicationLifetime.ApplicationStopping) .Token) - longRunningCancellationToken.Register (fun _ -> - socket - |> tryToGracefullyCloseSocketWithDefaultBehavior - |> Async.AwaitTask - |> Async.RunSynchronously) - |> ignore - let safe_HandleMessages = handleMessages longRunningCancellationToken + longRunningCancellationToken.Register (fun _ -> (socket |> tryToGracefullyCloseSocketWithDefaultBehavior).Wait()) |> ignore try - do! - socket - |> safe_HandleMessages - ctx - options.SerializerOptions - options.SchemaExecutor - options.RootFactory - options.WebsocketOptions.CustomPingHandler + do! socket |> handleMessages longRunningCancellationToken ctx with ex -> logger.LogError (ex, "Cannot handle Websocket message.") else diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs index 82a649f50..93db7cd40 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Messages.fs @@ -11,19 +11,19 @@ type SubscriptionUnsubscriber = IDisposable type OnUnsubscribeAction = SubscriptionId -> unit type SubscriptionsDict = IDictionary -type RawMessage = { Id : string option; Type : string; Payload : JsonDocument option } +type RawMessage = { Id : string voption; Type : string; Payload : JsonDocument voption } type ServerRawPayload = | ExecutionResult of Output | ErrorMessages of NameValueLookup list | CustomResponse of JsonDocument -type RawServerMessage = { Id : string option; Type : string; Payload : ServerRawPayload option } +type RawServerMessage = { Id : string voption; Type : string; Payload : ServerRawPayload voption } type ClientMessage = - | ConnectionInit of payload : JsonDocument option - | ClientPing of payload : JsonDocument option - | ClientPong of payload : JsonDocument option + | ConnectionInit of payload : JsonDocument voption + | ClientPing of payload : JsonDocument voption + | ClientPong of payload : JsonDocument voption | Subscribe of id : string * query : GQLRequestContent | ClientComplete of id : string @@ -32,7 +32,7 @@ type ClientMessageProtocolFailure = InvalidMessage of code : int * explanation : type ServerMessage = | ConnectionAck | ServerPing - | ServerPong of JsonDocument option + | ServerPong of JsonDocument voption | Next of id : string * payload : Output | Error of id : string * err : NameValueLookup list | Complete of id : string diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index cd2390945..36380b233 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -27,9 +27,9 @@ type ClientMessageConverter () = let getOptionalString (reader : byref) = if reader.TokenType.Equals (JsonTokenType.Null) then - None + ValueNone else - Some (reader.GetString ()) + ValueSome (reader.GetString ()) let readPropertyValueAsAString (propertyName : string) (reader : byref) = if reader.Read () then @@ -40,20 +40,20 @@ type ClientMessageConverter () = let requireId (raw : RawMessage) : Result = match raw.Id with - | Some s -> Ok s - | None -> + | ValueSome s -> Ok s + | ValueNone -> invalidMsg <| "Property \"id\" is required for this message but was not present." let requireSubscribePayload (serializerOptions : JsonSerializerOptions) - (payload : JsonDocument option) + (payload : JsonDocument voption) : Result = match payload with - | None -> + | ValueNone -> invalidMsg <| "Payload is required for this message, but none was present." - | Some p -> + | ValueSome p -> try JsonSerializer.Deserialize(p, serializerOptions) |> Ok with @@ -65,20 +65,20 @@ type ClientMessageConverter () = if not (reader.TokenType.Equals (JsonTokenType.StartObject)) then raise (new JsonException ($"reader's first token was not \"%A{JsonTokenType.StartObject}\", but \"%A{reader.TokenType}\"")) else - let mutable id : string option = None - let mutable theType : string option = None - let mutable payload : JsonDocument option = None + let mutable id : string voption = ValueNone + let mutable theType : string voption = ValueNone + let mutable payload : JsonDocument voption = ValueNone while reader.Read () && (not (reader.TokenType.Equals (JsonTokenType.EndObject))) do match reader.GetString () with | "id" -> id <- readPropertyValueAsAString "id" &reader | "type" -> theType <- readPropertyValueAsAString "type" &reader - | "payload" -> payload <- Some <| JsonDocument.ParseValue (&reader) + | "payload" -> payload <- ValueSome <| JsonDocument.ParseValue (&reader) | other -> raiseInvalidMsg <| $"Unknown property \"%s{other}\"" match theType with - | None -> raiseInvalidMsg "Property \"type\" is missing" - | Some msgType -> { Id = id; Type = msgType; Payload = payload } + | ValueNone -> raiseInvalidMsg "Property \"type\" is missing" + | ValueSome msgType -> { Id = id; Type = msgType; Payload = payload } override __.Read (reader : byref, typeToConvert : Type, options : JsonSerializerOptions) : ClientMessage = let raw = readRawMessage (&reader, options) @@ -119,12 +119,12 @@ type RawServerMessageConverter () = writer.WriteStartObject () writer.WriteString ("type", value.Type) match value.Id with - | None -> () - | Some id -> writer.WriteString ("id", id) + | ValueNone -> () + | ValueSome id -> writer.WriteString ("id", id) match value.Payload with - | None -> () - | Some serverRawPayload -> + | ValueNone -> () + | ValueSome serverRawPayload -> match serverRawPayload with | ExecutionResult output -> writer.WritePropertyName ("payload") diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index 9c3e98f6f..d7f73db41 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -7,6 +7,7 @@ open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection open FSharp.Data.GraphQL +open Microsoft.Extensions.Options [] module ServiceCollectionExtensions = @@ -18,7 +19,7 @@ module ServiceCollectionExtensions = WebsocketOptions = { EndpointUrl = endpointUrl ConnectionInitTimeoutInMs = 3000 - CustomPingHandler = None + CustomPingHandler = ValueNone } } @@ -51,8 +52,18 @@ module ServiceCollectionExtensions = Json.configureDefaultSerializerOptions Seq.empty o.SerializerOptions ) ) - .AddSingleton>(options) - .AddSingleton>(fun sp -> sp.GetRequiredService>()) + .AddSingleton>>( + { new IOptionsFactory> with + member this.Create name = options + } + ) + .Configure>(Giraffe.HttpHandlers.IdentedOptionsName, (fun o -> o.SerializerOptions.WriteIndented <- true)) + .AddSingleton>(fun sp -> + { new IOptionsFactory with + member this.Create name = + sp.GetRequiredService>>().Get(name) + } + ) [] module ApplicationBuilderExtensions = diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs index db0406049..05bcab916 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/SerializationTests.fs @@ -17,7 +17,7 @@ let ``Deserializes ConnectionInit correctly`` () = let result = JsonSerializer.Deserialize (input, serializerOptions) match result with - | ConnectionInit None -> () // <-- expected + | ConnectionInit ValueNone -> () // <-- expected | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") [] @@ -39,7 +39,7 @@ let ``Deserializes ClientPing correctly`` () = let result = JsonSerializer.Deserialize (input, serializerOptions) match result with - | ClientPing None -> () // <-- expected + | ClientPing ValueNone -> () // <-- expected | other -> Assert.Fail ($"unexpected actual value '%A{other}'") [] @@ -61,7 +61,7 @@ let ``Deserializes ClientPong correctly`` () = let result = JsonSerializer.Deserialize (input, serializerOptions) match result with - | ClientPong None -> () // <-- expected + | ClientPong ValueNone -> () // <-- expected | other -> Assert.Fail ($"unexpected actual value: '%A{other}'") [] From 0cfd55cac674789126aec202d03a439a34397720 Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 17 Mar 2024 22:22:05 +0100 Subject: [PATCH 090/100] InvalidMessageException -> InvalidWebsocketMessageException --- src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs | 2 +- .../GraphQLWebsocketMiddleware.fs | 2 +- .../Serialization/JsonConverters.fs | 2 +- .../FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs index f10d837e1..a7252b3ac 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Exceptions.fs @@ -1,4 +1,4 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore -type InvalidMessageException (explanation : string) = +type InvalidWebsocketMessageException (explanation : string) = inherit System.Exception (explanation) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 3c6a9f834..18dab5340 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -48,7 +48,7 @@ type GraphQLWebSocketMiddleware<'Root> try return JsonSerializer.Deserialize (msg, serializerOptions) with - | :? InvalidMessageException as e -> return! Result.Error <| InvalidMessage (4400, e.Message.ToString ()) + | :? InvalidWebsocketMessageException as e -> return! Result.Error <| InvalidMessage (4400, e.Message.ToString ()) | :? JsonException as e -> if logger.IsEnabled (LogLevel.Debug) then logger.LogDebug (e.ToString ()) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs index 36380b233..69253da7b 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JsonConverters.fs @@ -11,7 +11,7 @@ open FSharp.Data.GraphQL.Server.AspNetCore.WebSockets type ClientMessageConverter () = inherit JsonConverter () - let raiseInvalidMsg explanation = raise <| InvalidMessageException explanation + let raiseInvalidMsg explanation = raise <| InvalidWebsocketMessageException explanation /// From the spec: "Receiving a message of a type or format which is not specified in this document will result in an immediate socket closure with the event 4400: <error-message>. /// The <error-message> can be vaguely descriptive on why the received message is invalid." diff --git a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs index 3ec862f88..7075962f9 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AspNetCore/InvalidMessageTests.fs @@ -15,7 +15,7 @@ let willResultInInvalidMessage expectedExplanation input = Assert.Fail (sprintf "should have failed, but succeeded with result: '%A'" result) with | :? JsonException as ex -> Assert.Equal (expectedExplanation, ex.Message) - | :? InvalidMessageException as ex -> Assert.Equal (expectedExplanation, ex.Message) + | :? InvalidWebsocketMessageException as ex -> Assert.Equal (expectedExplanation, ex.Message) let willResultInJsonException input = try From cd3b1ce6ee90e9c5f2ba191d3c891952a913bcad Mon Sep 17 00:00:00 2001 From: valber Date: Sun, 17 Mar 2024 22:37:26 +0100 Subject: [PATCH 091/100] .AspNetCore: using static logger serializer options and fixed typo: "Identation" -> Indentation --- .../Giraffe/HttpHandlers.fs | 18 +++++++----------- .../Serialization/JSON.fs | 9 +++++++++ .../StartupExtensions.fs | 1 - 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 0ceafc718..6d42334cd 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -20,9 +20,6 @@ open FSharp.Data.GraphQL.Server.AspNetCore type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult module HttpHandlers = - - let [] internal IdentedOptionsName = "Idented" - let rec private moduleType = getModuleType <@ moduleType @> let ofTaskIResult ctx (taskRes: Task) : HttpFuncResult = task { @@ -45,9 +42,8 @@ module HttpHandlers = let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } = - let serializeIdented value = - let jsonSerializerOptions = options.Get(IdentedOptionsName).SerializerOptions - JsonSerializer.Serialize(value, jsonSerializerOptions) + let serializeIndented value = + JsonSerializer.Serialize(value, Json.loggerSerializerOptions) match content with | Direct(data, errs) -> @@ -58,7 +54,7 @@ module HttpHandlers = ) if logger.IsEnabled LogLevel.Trace then - logger.LogTrace($"GraphQL response data:{Environment.NewLine}:{{data}}", serializeIdented data) + logger.LogTrace($"GraphQL response data:{Environment.NewLine}:{{data}}", serializeIndented data) GQLResponse.Direct(documentId, data, errs) | Deferred(data, errs, deferred) -> @@ -80,7 +76,7 @@ module HttpHandlers = if logger.IsEnabled LogLevel.Trace then logger.LogTrace( $"GraphQL deferred data:{Environment.NewLine}{{data}}", - serializeIdented data + serializeIndented data ) | DeferredErrors(null, errors, path) -> logger.LogDebug( @@ -100,7 +96,7 @@ module HttpHandlers = logger.LogTrace( $"GraphQL deferred errors:{Environment.NewLine}{{errors}}{Environment.NewLine}GraphQL deferred data:{Environment.NewLine}{{data}}", errors, - serializeIdented data + serializeIndented data )) GQLResponse.Direct(documentId, data, errs) @@ -120,7 +116,7 @@ module HttpHandlers = if logger.IsEnabled LogLevel.Trace then logger.LogTrace( $"GraphQL subscription data:{Environment.NewLine}{{data}}", - serializeIdented data + serializeIndented data ) | SubscriptionErrors(null, errors) -> logger.LogDebug("Produced GraphQL subscription errors") @@ -134,7 +130,7 @@ module HttpHandlers = logger.LogTrace( $"GraphQL subscription errors:{Environment.NewLine}{{errors}}{Environment.NewLine}GraphQL deferred data:{Environment.NewLine}{{data}}", errors, - serializeIdented data + serializeIndented data )) GQLResponse.Stream documentId diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs index 2d949f2ac..0f5330c70 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs @@ -36,6 +36,15 @@ let defaultJsonFSharpOptions = UnionTag, allowOverride = true) +let loggerSerializerOptions = + let options = JsonSerializerOptions() + options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase + options.PropertyNameCaseInsensitive <- true + let converters = options.Converters + converters.Add (new JsonStringEnumConverter ()) + defaultJsonFSharpOptions.AddToJsonSerializerOptions options + options + let configureDefaultSerializerOptions = configureSerializerOptions defaultJsonFSharpOptions let configureDefaultWSSerializerOptions = configureWSSerializerOptions defaultJsonFSharpOptions diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index d7f73db41..4bd531f82 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -57,7 +57,6 @@ module ServiceCollectionExtensions = member this.Create name = options } ) - .Configure>(Giraffe.HttpHandlers.IdentedOptionsName, (fun o -> o.SerializerOptions.WriteIndented <- true)) .AddSingleton>(fun sp -> { new IOptionsFactory with member this.Create name = From c56672453a875b9be8d9aadaabd4857f316fbbac Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 20 Mar 2024 22:54:04 +0100 Subject: [PATCH 092/100] Revert ".AspNetCore: using static logger serializer options and fixed typo:" This reverts commit cd3b1ce6ee90e9c5f2ba191d3c891952a913bcad. --- .../Giraffe/HttpHandlers.fs | 18 +++++++++++------- .../Serialization/JSON.fs | 9 --------- .../StartupExtensions.fs | 1 + 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 6d42334cd..0ceafc718 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -20,6 +20,9 @@ open FSharp.Data.GraphQL.Server.AspNetCore type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult module HttpHandlers = + + let [] internal IdentedOptionsName = "Idented" + let rec private moduleType = getModuleType <@ moduleType @> let ofTaskIResult ctx (taskRes: Task) : HttpFuncResult = task { @@ -42,8 +45,9 @@ module HttpHandlers = let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } = - let serializeIndented value = - JsonSerializer.Serialize(value, Json.loggerSerializerOptions) + let serializeIdented value = + let jsonSerializerOptions = options.Get(IdentedOptionsName).SerializerOptions + JsonSerializer.Serialize(value, jsonSerializerOptions) match content with | Direct(data, errs) -> @@ -54,7 +58,7 @@ module HttpHandlers = ) if logger.IsEnabled LogLevel.Trace then - logger.LogTrace($"GraphQL response data:{Environment.NewLine}:{{data}}", serializeIndented data) + logger.LogTrace($"GraphQL response data:{Environment.NewLine}:{{data}}", serializeIdented data) GQLResponse.Direct(documentId, data, errs) | Deferred(data, errs, deferred) -> @@ -76,7 +80,7 @@ module HttpHandlers = if logger.IsEnabled LogLevel.Trace then logger.LogTrace( $"GraphQL deferred data:{Environment.NewLine}{{data}}", - serializeIndented data + serializeIdented data ) | DeferredErrors(null, errors, path) -> logger.LogDebug( @@ -96,7 +100,7 @@ module HttpHandlers = logger.LogTrace( $"GraphQL deferred errors:{Environment.NewLine}{{errors}}{Environment.NewLine}GraphQL deferred data:{Environment.NewLine}{{data}}", errors, - serializeIndented data + serializeIdented data )) GQLResponse.Direct(documentId, data, errs) @@ -116,7 +120,7 @@ module HttpHandlers = if logger.IsEnabled LogLevel.Trace then logger.LogTrace( $"GraphQL subscription data:{Environment.NewLine}{{data}}", - serializeIndented data + serializeIdented data ) | SubscriptionErrors(null, errors) -> logger.LogDebug("Produced GraphQL subscription errors") @@ -130,7 +134,7 @@ module HttpHandlers = logger.LogTrace( $"GraphQL subscription errors:{Environment.NewLine}{{errors}}{Environment.NewLine}GraphQL deferred data:{Environment.NewLine}{{data}}", errors, - serializeIndented data + serializeIdented data )) GQLResponse.Stream documentId diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs index 0f5330c70..2d949f2ac 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Serialization/JSON.fs @@ -36,15 +36,6 @@ let defaultJsonFSharpOptions = UnionTag, allowOverride = true) -let loggerSerializerOptions = - let options = JsonSerializerOptions() - options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase - options.PropertyNameCaseInsensitive <- true - let converters = options.Converters - converters.Add (new JsonStringEnumConverter ()) - defaultJsonFSharpOptions.AddToJsonSerializerOptions options - options - let configureDefaultSerializerOptions = configureSerializerOptions defaultJsonFSharpOptions let configureDefaultWSSerializerOptions = configureWSSerializerOptions defaultJsonFSharpOptions diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index 4bd531f82..d7f73db41 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -57,6 +57,7 @@ module ServiceCollectionExtensions = member this.Create name = options } ) + .Configure>(Giraffe.HttpHandlers.IdentedOptionsName, (fun o -> o.SerializerOptions.WriteIndented <- true)) .AddSingleton>(fun sp -> { new IOptionsFactory with member this.Create name = From 418aa7a411fc89e2a856f47c4d17d5ae5aaba209 Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 20 Mar 2024 22:55:26 +0100 Subject: [PATCH 093/100] .AspNetCore: fixed typo: Idented -> Indented --- .../Giraffe/HttpHandlers.fs | 16 ++++++++-------- .../StartupExtensions.fs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 0ceafc718..802089c2f 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -21,7 +21,7 @@ type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult module HttpHandlers = - let [] internal IdentedOptionsName = "Idented" + let [] internal IndentedOptionsName = "Indented" let rec private moduleType = getModuleType <@ moduleType @> @@ -45,8 +45,8 @@ module HttpHandlers = let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } = - let serializeIdented value = - let jsonSerializerOptions = options.Get(IdentedOptionsName).SerializerOptions + let serializeIndented value = + let jsonSerializerOptions = options.Get(IndentedOptionsName).SerializerOptions JsonSerializer.Serialize(value, jsonSerializerOptions) match content with @@ -58,7 +58,7 @@ module HttpHandlers = ) if logger.IsEnabled LogLevel.Trace then - logger.LogTrace($"GraphQL response data:{Environment.NewLine}:{{data}}", serializeIdented data) + logger.LogTrace($"GraphQL response data:{Environment.NewLine}:{{data}}", serializeIndented data) GQLResponse.Direct(documentId, data, errs) | Deferred(data, errs, deferred) -> @@ -80,7 +80,7 @@ module HttpHandlers = if logger.IsEnabled LogLevel.Trace then logger.LogTrace( $"GraphQL deferred data:{Environment.NewLine}{{data}}", - serializeIdented data + serializeIndented data ) | DeferredErrors(null, errors, path) -> logger.LogDebug( @@ -100,7 +100,7 @@ module HttpHandlers = logger.LogTrace( $"GraphQL deferred errors:{Environment.NewLine}{{errors}}{Environment.NewLine}GraphQL deferred data:{Environment.NewLine}{{data}}", errors, - serializeIdented data + serializeIndented data )) GQLResponse.Direct(documentId, data, errs) @@ -120,7 +120,7 @@ module HttpHandlers = if logger.IsEnabled LogLevel.Trace then logger.LogTrace( $"GraphQL subscription data:{Environment.NewLine}{{data}}", - serializeIdented data + serializeIndented data ) | SubscriptionErrors(null, errors) -> logger.LogDebug("Produced GraphQL subscription errors") @@ -134,7 +134,7 @@ module HttpHandlers = logger.LogTrace( $"GraphQL subscription errors:{Environment.NewLine}{{errors}}{Environment.NewLine}GraphQL deferred data:{Environment.NewLine}{{data}}", errors, - serializeIdented data + serializeIndented data )) GQLResponse.Stream documentId diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index d7f73db41..f539b73b7 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -57,7 +57,7 @@ module ServiceCollectionExtensions = member this.Create name = options } ) - .Configure>(Giraffe.HttpHandlers.IdentedOptionsName, (fun o -> o.SerializerOptions.WriteIndented <- true)) + .Configure>(Giraffe.HttpHandlers.IndentedOptionsName, (fun o -> o.SerializerOptions.WriteIndented <- true)) .AddSingleton>(fun sp -> { new IOptionsFactory with member this.Create name = From c9951c38f90518bba32d9f9686f46895a6c10776 Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 20 Mar 2024 23:22:31 +0100 Subject: [PATCH 094/100] .WebsocketMiddleware: logging with logger instead of `printfn` --- .../GraphQLWebsocketMiddleware.fs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 18dab5340..005114498 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -167,7 +167,10 @@ type GraphQLWebSocketMiddleware<'Root> match subscriptionResult with | SubscriptionResult output -> output |> sendOutput id | SubscriptionErrors (output, errors) -> - printfn "Subscription errors: %s" (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) + logger.LogWarning( + "Subscription errors: {subscriptionerrors}", + (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) + ) Task.FromResult (()) let sendDeferredResponseOutput id deferredResult = @@ -176,7 +179,10 @@ type GraphQLWebSocketMiddleware<'Root> let output = obj :?> Dictionary output |> sendOutput id | DeferredErrors (obj, errors, _) -> - printfn "Deferred response errors: %s" (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) + logger.LogWarning( + "Deferred response errors: {deferrederrors}", + (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) + ) Task.FromResult (()) let sendDeferredResultDelayedBy (cancToken : CancellationToken) (ms : int) id deferredResult : Task = task { @@ -198,7 +204,11 @@ type GraphQLWebSocketMiddleware<'Root> else () | Direct (data, _) -> do! data |> sendOutput id - | RequestError problemDetails -> printfn "Request error: %s" (String.Join ('\n', problemDetails |> Seq.map (fun x -> $"- %s{x.Message}"))) + | RequestError problemDetails -> + logger.LogWarning( + "Request error: %s", + (String.Join ('\n', problemDetails |> Seq.map (fun x -> $"- %s{x.Message}"))) + ) } let getStrAddendumOfOptionalPayload optionalPayload = From 855d5a7a4fd83d6eb08222dcae26b24b60e25cd5 Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 20 Mar 2024 23:25:08 +0100 Subject: [PATCH 095/100] .WebsocketMiddleware: logging it when subscriber already exists According to suggestion during code review. --- .../GraphQLWebsocketMiddleware.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 005114498..dd42e037c 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -263,9 +263,11 @@ type GraphQLWebSocketMiddleware<'Root> "Subscribe" |> logMsgWithIdReceived id if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then do! + let warningMsg = $"Subscriber for %s{id} already exists" + logger.LogWarning(warningMsg) socket.CloseAsync ( enum CustomWebSocketStatus.SubscriberAlreadyExists, - $"Subscriber for %s{id} already exists", + warningMsg, CancellationToken.None ) else From 5dde6e9b4d7bb159a45a13a09918755f82f4d6b2 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 21 Mar 2024 22:09:36 +0400 Subject: [PATCH 096/100] Aligned error and logging messages, simplified WebSockets logging code --- .../Giraffe/HttpHandlers.fs | 26 +- .../GraphQLWebsocketMiddleware.fs | 223 +++++++++--------- .../FSharp.Data.GraphQL.Tests/ErrorHelpers.fs | 6 +- .../ExecutionTests.fs | 2 +- tests/FSharp.Data.GraphQL.Tests/Helpers.fs | 2 +- .../MiddlewareTests.fs | 2 +- .../MutationTests.fs | 6 +- .../Relay/CursorTests.fs | 2 +- .../InputObjectValidatorTests.fs | 2 +- 9 files changed, 131 insertions(+), 140 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs index 802089c2f..6434951af 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Giraffe/HttpHandlers.fs @@ -52,18 +52,18 @@ module HttpHandlers = match content with | Direct(data, errs) -> logger.LogDebug( - $"Produced direct GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", + $"Produced direct GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", documentId, metadata ) if logger.IsEnabled LogLevel.Trace then - logger.LogTrace($"GraphQL response data:{Environment.NewLine}:{{data}}", serializeIndented data) + logger.LogTrace($"GraphQL response data:\n:{{data}}", serializeIndented data) GQLResponse.Direct(documentId, data, errs) | Deferred(data, errs, deferred) -> logger.LogDebug( - $"Produced deferred GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", + $"Produced deferred GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", documentId, metadata ) @@ -79,7 +79,7 @@ module HttpHandlers = if logger.IsEnabled LogLevel.Trace then logger.LogTrace( - $"GraphQL deferred data:{Environment.NewLine}{{data}}", + $"GraphQL deferred data:\n{{data}}", serializeIndented data ) | DeferredErrors(null, errors, path) -> @@ -89,7 +89,7 @@ module HttpHandlers = ) if logger.IsEnabled LogLevel.Trace then - logger.LogTrace($"GraphQL deferred errors:{Environment.NewLine}{{errors}}", errors) + logger.LogTrace($"GraphQL deferred errors:\n{{errors}}", errors) | DeferredErrors(data, errors, path) -> logger.LogDebug( "Produced GraphQL deferred result with errors for path: {path}", @@ -98,7 +98,7 @@ module HttpHandlers = if logger.IsEnabled LogLevel.Trace then logger.LogTrace( - $"GraphQL deferred errors:{Environment.NewLine}{{errors}}{Environment.NewLine}GraphQL deferred data:{Environment.NewLine}{{data}}", + $"GraphQL deferred errors:\n{{errors}}\nGraphQL deferred data:\n{{data}}", errors, serializeIndented data )) @@ -106,7 +106,7 @@ module HttpHandlers = GQLResponse.Direct(documentId, data, errs) | Stream stream -> logger.LogDebug( - $"Produced stream GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", + $"Produced stream GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", documentId, metadata ) @@ -119,20 +119,20 @@ module HttpHandlers = if logger.IsEnabled LogLevel.Trace then logger.LogTrace( - $"GraphQL subscription data:{Environment.NewLine}{{data}}", + $"GraphQL subscription data:\n{{data}}", serializeIndented data ) | SubscriptionErrors(null, errors) -> logger.LogDebug("Produced GraphQL subscription errors") if logger.IsEnabled LogLevel.Trace then - logger.LogTrace($"GraphQL subscription errors:{Environment.NewLine}{{errors}}", errors) + logger.LogTrace($"GraphQL subscription errors:\n{{errors}}", errors) | SubscriptionErrors(data, errors) -> logger.LogDebug("Produced GraphQL subscription result with errors") if logger.IsEnabled LogLevel.Trace then logger.LogTrace( - $"GraphQL subscription errors:{Environment.NewLine}{{errors}}{Environment.NewLine}GraphQL deferred data:{Environment.NewLine}{{data}}", + $"GraphQL subscription errors:\n{{errors}}\nGraphQL deferred data:\n{{data}}", errors, serializeIndented data )) @@ -140,7 +140,7 @@ module HttpHandlers = GQLResponse.Stream documentId | RequestError errs -> logger.LogWarning( - $"Produced request error GraphQL response with documentId = '{{documentId}}' and metadata:{Environment.NewLine}{{metadata}}", + $"Produced request error GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", documentId, metadata ) @@ -241,10 +241,10 @@ module HttpHandlers = operationName |> Option.iter (fun on -> logger.LogTrace("GraphQL operation name: '{operationName}'", on)) - logger.LogTrace($"Executing GraphQL query:{Environment.NewLine}{{query}}", content.Query) + logger.LogTrace($"Executing GraphQL query:\n{{query}}", content.Query) variables - |> Option.iter (fun v -> logger.LogTrace($"GraphQL variables:{Environment.NewLine}{{variables}}", v)) + |> Option.iter (fun v -> logger.LogTrace($"GraphQL variables:\n{{variables}}", v)) let root = options.CurrentValue.RootFactory ctx diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index dd42e037c..9c867313a 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -56,7 +56,7 @@ type GraphQLWebSocketMiddleware<'Root> () return! Result.Error - <| InvalidMessage (4400, "invalid json in client message") + <| InvalidMessage (4400, "Invalid JSON in the client message") } let isSocketOpen (theSocket : WebSocket) = @@ -98,13 +98,13 @@ type GraphQLWebSocketMiddleware<'Root> let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) : Task = task { if not (socket.State = WebSocketState.Open) then - logger.LogTrace ("Ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) + logger.LogTrace ($"Ignoring message to be sent via socket, since its state is not '{nameof WebSocketState.Open}', but '{{state}}'", socket.State) else // TODO: Allocate string only if a debugger is attached let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions let segment = new ArraySegment (System.Text.Encoding.UTF8.GetBytes (serializedMessage)) if not (socket.State = WebSocketState.Open) then - logger.LogTrace ("ignoring message to be sent via socket, since its state is not 'Open', but '{state}'", socket.State) + logger.LogTrace ($"Ignoring message to be sent via socket, since its state is not '{nameof WebSocketState.Open}', but '{{state}}'", socket.State) else do! socket.SendAsync (segment, WebSocketMessageType.Text, endOfMessage = true, cancellationToken = CancellationToken.None) @@ -121,8 +121,8 @@ type GraphQLWebSocketMiddleware<'Root> = let observer = new Reactive.AnonymousObserver<'ResponseContent> ( - onNext = (fun theOutput -> (howToSendDataOnNext id theOutput).Wait()), - onError = (fun ex -> logger.LogError (ex, "Error on subscription with id='{id}'", id)), + onNext = (fun theOutput -> (howToSendDataOnNext id theOutput).Wait ()), + onError = (fun ex -> logger.LogError (ex, "Error on subscription with Id = '{id}'", id)), onCompleted = (fun () -> (sendMessageViaSocket jsonSerializerOptions socket (Complete id)).Wait () @@ -155,7 +155,7 @@ type GraphQLWebSocketMiddleware<'Root> let rcv () = socket |> rcvMsgViaSocket serializerOptions let sendOutput id (output : Output) = - match output.TryGetValue ("errors") with + match output.TryGetValue "errors" with | true, theValue -> // The specification says: "This message terminates the operation and no further messages will be sent." subscriptions @@ -167,11 +167,8 @@ type GraphQLWebSocketMiddleware<'Root> match subscriptionResult with | SubscriptionResult output -> output |> sendOutput id | SubscriptionErrors (output, errors) -> - logger.LogWarning( - "Subscription errors: {subscriptionerrors}", - (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) - ) - Task.FromResult (()) + logger.LogWarning ("Subscription errors: {subscriptionErrors}", (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}")))) + Task.FromResult () let sendDeferredResponseOutput id deferredResult = match deferredResult with @@ -179,17 +176,16 @@ type GraphQLWebSocketMiddleware<'Root> let output = obj :?> Dictionary output |> sendOutput id | DeferredErrors (obj, errors, _) -> - logger.LogWarning( - "Deferred response errors: {deferrederrors}", + logger.LogWarning ( + "Deferred response errors: {deferredErrors}", (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) ) - Task.FromResult (()) + Task.FromResult () - let sendDeferredResultDelayedBy (cancToken : CancellationToken) (ms : int) id deferredResult : Task = task { - do! Async.StartAsTask (Async.Sleep ms, cancellationToken = cancToken) + let sendDeferredResultDelayedBy (ct : CancellationToken) (ms : int) id deferredResult : Task = task { + do! Task.Delay (ms, ct) do! deferredResult |> sendDeferredResponseOutput id } - let sendQueryOutputDelayedBy = sendDeferredResultDelayedBy cancellationToken let applyPlanExecutionResult (id : SubscriptionId) (socket) (executionResult : GQLExecutionResult) : Task = task { match executionResult with @@ -200,7 +196,7 @@ type GraphQLWebSocketMiddleware<'Root> do! data |> sendOutput id if errors.IsEmpty then (subscriptions, socket, observableOutput, serializerOptions) - |> addClientSubscription id (sendQueryOutputDelayedBy 5000) + |> addClientSubscription id (sendDeferredResultDelayedBy cancellationToken 5000) else () | Direct (data, _) -> do! data |> sendOutput id @@ -211,15 +207,12 @@ type GraphQLWebSocketMiddleware<'Root> ) } - let getStrAddendumOfOptionalPayload optionalPayload = - optionalPayload - |> ValueOption.map (fun payloadStr -> $" with payload: %A{payloadStr}") - |> ValueOption.defaultWith (fun () -> "") - let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) = - logger.LogTrace ("{message}{messageaddendum}", msgAsStr, (optionalPayload |> getStrAddendumOfOptionalPayload)) + match optionalPayload with + | ValueSome payload -> logger.LogTrace ($"{msgAsStr} with payload\n{{messageAddendum}}", (payload : 'Payload)) + | ValueNone -> logger.LogTrace (msgAsStr) - let logMsgWithIdReceived (id : string) (msgAsStr : string) = logger.LogTrace ("{message} (id: {messageid})", msgAsStr, id) + let logMsgWithIdReceived (id : string) (msgAsStr : string) = logger.LogTrace ($"{msgAsStr}. Id = '{{messageId}}'", id) // <-------------- // <-- Helpers --| @@ -235,52 +228,51 @@ type GraphQLWebSocketMiddleware<'Root> let! receivedMessage = rcv () match receivedMessage with | Result.Error failureMsgs -> - "InvalidMessage" |> logMsgReceivedWithOptionalPayload ValueNone + nameof InvalidMessage + |> logMsgReceivedWithOptionalPayload ValueNone match failureMsgs with | InvalidMessage (code, explanation) -> do! socket.CloseAsync (enum code, explanation, CancellationToken.None) - | Ok maybeMsg -> - match maybeMsg with - | ValueNone -> logger.LogTrace ("Websocket socket received empty message! (socket state = {socketstate})", socket.State) - | ValueSome msg -> - match msg with - | ConnectionInit p -> - "ConnectionInit" |> logMsgReceivedWithOptionalPayload p + | Ok ValueNone -> logger.LogTrace ("WebSocket received empty message! State = '{socketState}'", socket.State) + | Ok (ValueSome msg) -> + match msg with + | ConnectionInit p -> + nameof ConnectionInit |> logMsgReceivedWithOptionalPayload p + do! + socket.CloseAsync ( + enum CustomWebSocketStatus.TooManyInitializationRequests, + "Too many initialization requests", + CancellationToken.None + ) + | ClientPing p -> + nameof ClientPing |> logMsgReceivedWithOptionalPayload p + match pingHandler with + | ValueSome func -> + let! customP = p |> func serviceProvider + do! ServerPong customP |> sendMsg + | ValueNone -> do! ServerPong p |> sendMsg + | ClientPong p -> nameof ClientPong |> logMsgReceivedWithOptionalPayload p + | Subscribe (id, query) -> + nameof Subscribe |> logMsgWithIdReceived id + if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then do! + let warningMsg : FormattableString = $"Subscriber for Id = '{id}' already exists" + logger.LogWarning (String.Format (warningMsg.Format, "id"), id) socket.CloseAsync ( - enum CustomWebSocketStatus.TooManyInitializationRequests, - "too many initialization requests", + enum CustomWebSocketStatus.SubscriberAlreadyExists, + warningMsg.ToString (), CancellationToken.None ) - | ClientPing p -> - "ClientPing" |> logMsgReceivedWithOptionalPayload p - match pingHandler with - | ValueSome func -> - let! customP = p |> func serviceProvider - do! ServerPong customP |> sendMsg - | ValueNone -> do! ServerPong p |> sendMsg - | ClientPong p -> "ClientPong" |> logMsgReceivedWithOptionalPayload p - | Subscribe (id, query) -> - "Subscribe" |> logMsgWithIdReceived id - if subscriptions |> GraphQLSubscriptionsManagement.isIdTaken id then - do! - let warningMsg = $"Subscriber for %s{id} already exists" - logger.LogWarning(warningMsg) - socket.CloseAsync ( - enum CustomWebSocketStatus.SubscriberAlreadyExists, - warningMsg, - CancellationToken.None - ) - else - let variables = query.Variables |> Skippable.toOption - let! planExecutionResult = - let root = options.RootFactory httpContext - options.SchemaExecutor.AsyncExecute (query.Query, root, ?variables = variables) - do! planExecutionResult |> applyPlanExecutionResult id socket - | ClientComplete id -> - "ClientComplete" |> logMsgWithIdReceived id - subscriptions - |> GraphQLSubscriptionsManagement.removeSubscription (id) - logger.LogTrace "Leaving graphql-ws connection loop..." + else + let variables = query.Variables |> Skippable.toOption + let! planExecutionResult = + let root = options.RootFactory httpContext + options.SchemaExecutor.AsyncExecute (query.Query, root, ?variables = variables) + do! planExecutionResult |> applyPlanExecutionResult id socket + | ClientComplete id -> + "ClientComplete" |> logMsgWithIdReceived id + subscriptions + |> GraphQLSubscriptionsManagement.removeSubscription (id) + logger.LogTrace "Leaving the 'graphql-ws' connection loop..." do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior with ex -> logger.LogError (ex, "Cannot handle a message; dropping a websocket connection") @@ -294,73 +286,72 @@ type GraphQLWebSocketMiddleware<'Root> // <-- Main // <-------- - let waitForConnectionInitAndRespondToClient (socket : WebSocket) : TaskResult = - task { - let timerTokenSource = new CancellationTokenSource () - timerTokenSource.CancelAfter connectionInitTimeout - let detonationRegistration = - timerTokenSource.Token.Register (fun _ -> - socket - |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.ConnectionTimeout, "Connection initialization timeout") - |> Task.WaitAll) - - let! connectionInitSucceeded = - TaskResult.Run ( - (fun _ -> task { - logger.LogDebug ("Waiting for ConnectionInit...") - let! receivedMessage = receiveMessageViaSocket (CancellationToken.None) serializerOptions socket - match receivedMessage with - | Ok (ValueSome (ConnectionInit _)) -> - logger.LogDebug ("Valid connection_init received! Responding with ACK!") - detonationRegistration.Unregister () |> ignore - do! - ConnectionAck - |> sendMessageViaSocket serializerOptions socket - return true - | Ok (ValueSome (Subscribe _)) -> - do! - socket - |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.Unauthorized, "Unauthorized") - return false - | Result.Error (InvalidMessage (code, explanation)) -> - do! - socket - |> tryToGracefullyCloseSocket (enum code, explanation) - return false - | _ -> - do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior - return false - }), - timerTokenSource.Token - ) - if (not timerTokenSource.Token.IsCancellationRequested) then - if connectionInitSucceeded then - return Ok () - else - return Result.Error ("ConnectionInit failed (not because of timeout)") + let waitForConnectionInitAndRespondToClient (socket : WebSocket) : TaskResult = task { + let timerTokenSource = new CancellationTokenSource () + timerTokenSource.CancelAfter connectionInitTimeout + let detonationRegistration = + timerTokenSource.Token.Register (fun _ -> + (socket + |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.ConnectionTimeout, "Connection initialization timeout")) + .Wait ()) + + let! connectionInitSucceeded = + TaskResult.Run ( + (fun _ -> task { + logger.LogDebug ($"Waiting for {nameof ConnectionInit}...") + let! receivedMessage = receiveMessageViaSocket CancellationToken.None serializerOptions socket + match receivedMessage with + | Ok (ValueSome (ConnectionInit _)) -> + logger.LogDebug ($"Valid {nameof ConnectionInit} received! Responding with ACK!") + detonationRegistration.Unregister () |> ignore + do! + ConnectionAck + |> sendMessageViaSocket serializerOptions socket + return true + | Ok (ValueSome (Subscribe _)) -> + do! + socket + |> tryToGracefullyCloseSocket (enum CustomWebSocketStatus.Unauthorized, "Unauthorized") + return false + | Result.Error (InvalidMessage (code, explanation)) -> + do! + socket + |> tryToGracefullyCloseSocket (enum code, explanation) + return false + | _ -> + do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior + return false + }), + timerTokenSource.Token + ) + if (not timerTokenSource.Token.IsCancellationRequested) then + if connectionInitSucceeded then + return Ok () else - return Result.Error <| "ConnectionInit timeout" - } + return Result.Error ($"{nameof ConnectionInit} failed (not because of timeout)") + else + return Result.Error <| "{nameof ConnectionInit} timeout" + } member __.InvokeAsync (ctx : HttpContext) = task { if not (ctx.Request.Path = endpointUrl) then do! next.Invoke (ctx) else if ctx.WebSockets.IsWebSocketRequest then use! socket = ctx.WebSockets.AcceptWebSocketAsync ("graphql-transport-ws") - let! connectionInitResult = - socket |> waitForConnectionInitAndRespondToClient + let! connectionInitResult = socket |> waitForConnectionInitAndRespondToClient match connectionInitResult with - | Result.Error errMsg -> logger.LogWarning ("{warningmsg}", errMsg) + | Result.Error errMsg -> logger.LogWarning errMsg | Ok _ -> let longRunningCancellationToken = (CancellationTokenSource .CreateLinkedTokenSource(ctx.RequestAborted, applicationLifetime.ApplicationStopping) .Token) - longRunningCancellationToken.Register (fun _ -> (socket |> tryToGracefullyCloseSocketWithDefaultBehavior).Wait()) |> ignore + longRunningCancellationToken.Register (fun _ -> (socket |> tryToGracefullyCloseSocketWithDefaultBehavior).Wait ()) + |> ignore try do! socket |> handleMessages longRunningCancellationToken ctx with ex -> - logger.LogError (ex, "Cannot handle Websocket message.") + logger.LogError (ex, "Cannot handle WebSocket message.") else do! next.Invoke (ctx) } diff --git a/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs b/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs index 2feb909ff..2b8a7407b 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs @@ -14,17 +14,17 @@ type ErrorSource = let ensureDeferred (result : GQLExecutionResult) (onDeferred : Output -> GQLProblemDetails list -> IObservable -> unit) : unit = match result.Content with | Deferred(data, errors, deferred) -> onDeferred data errors deferred - | response -> fail $"Expected a Deferred GQLResponse but got {Environment.NewLine}{response}" + | response -> fail $"Expected a 'Deferred' GQLResponse but got\n{response}" let ensureDirect (result : GQLExecutionResult) (onDirect : Output -> GQLProblemDetails list -> unit) : unit = match result.Content with | Direct(data, errors) -> onDirect data errors - | response -> fail $"Expected a Direct GQLResponse but got {Environment.NewLine}{response}" + | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}" let ensureRequestError (result : GQLExecutionResult) (onRequestError : GQLProblemDetails list -> unit) : unit = match result.Content with | RequestError errors -> onRequestError errors - | response -> fail $"Expected RequestError GQLResponse but got {Environment.NewLine}{response}" + | response -> fail $"Expected 'RequestError' GQLResponse but got\n{response}" let ensureValidationError (message : string) (path : FieldPath) (error : GQLProblemDetails) = equals message error.Message diff --git a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs index 7afa11e95..99ffafa01 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs @@ -380,7 +380,7 @@ let ``Execution when querying returns unique document id with response`` () = | Direct(data1, errors1), Direct(data2, errors2) -> equals data1 data2 equals errors1 errors2 - | response -> fail $"Expected a Direct GQLResponse but got {Environment.NewLine}{response}" + | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}" type InnerNullableTest = { Kaboom : string } type NullableTest = { Inner : InnerNullableTest } diff --git a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs index 7d304ebd1..ebed4962d 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs @@ -18,7 +18,7 @@ let isDict<'k, 'v> actual = isSeq> actual let isNameValueDict actual = isDict actual let fail (message: string) = Assert.Fail message let equals (expected : 'x) (actual : 'x) = - if not (actual = expected) then fail <| $"expected %A{expected}{Environment.NewLine}but got %A{actual}" + if not (actual = expected) then fail <| $"expected %A{expected}\nbut got %A{actual}" let notEquals (expected : 'x) (actual : 'x) = if actual = expected then fail <| $"unexpected %+A{expected}" let noErrors (result: IDictionary) = diff --git a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs index 473dc8c0b..aaf31f99e 100644 --- a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs @@ -455,7 +455,7 @@ let ``Inline fragment query : Should not pass when above threshold``() = let result = execute query match result with | RequestError errors -> errors |> equals expectedErrors - | response -> fail $"Expected RequestError GQLResponse but got {Environment.NewLine}{response}" + | response -> fail $"Expected 'RequestError' GQLResponse but got\n{response}" ensureRequestError result <| fun errors -> errors |> equals expectedErrors diff --git a/tests/FSharp.Data.GraphQL.Tests/MutationTests.fs b/tests/FSharp.Data.GraphQL.Tests/MutationTests.fs index c2ad52cc2..def1055f7 100644 --- a/tests/FSharp.Data.GraphQL.Tests/MutationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/MutationTests.fs @@ -76,7 +76,7 @@ let ``Execute handles mutation execution ordering: evaluates mutations serially` | Direct(data, errors) -> empty errors data |> equals (upcast expected) - | response -> fail $"Expected a Direct GQLResponse but got {Environment.NewLine}{response}" + | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}" [] let ``Execute handles mutation execution ordering: evaluates mutations correctly in the presense of failures`` () = @@ -117,7 +117,7 @@ let ``Execute handles mutation execution ordering: evaluates mutations correctly | Direct(data, errors) -> data |> equals (upcast expected) List.length errors |> equals 2 - | response -> fail $"Expected a Direct GQLResponse but got {Environment.NewLine}{response}" + | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}" //[] //let ``Execute handles mutation with multiple arguments`` () = @@ -136,4 +136,4 @@ let ``Execute handles mutation execution ordering: evaluates mutations correctly // | Direct(data, errors) -> // empty errors // data |> equals (upcast expected) -// | response -> fail $"Expected a Direct GQLResponse but got {Environment.NewLine}{response}" +// | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}" diff --git a/tests/FSharp.Data.GraphQL.Tests/Relay/CursorTests.fs b/tests/FSharp.Data.GraphQL.Tests/Relay/CursorTests.fs index ea295b44f..74172dac6 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Relay/CursorTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Relay/CursorTests.fs @@ -99,4 +99,4 @@ let ``Relay cursor works for types with nested fileds`` () = match result with | Direct (_, errors) -> empty errors - | response -> fail $"Expected a Direct GQLResponse but got {Environment.NewLine}{response}" + | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}" diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputObjectValidatorTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputObjectValidatorTests.fs index 54a850115..d9bb21a16 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputObjectValidatorTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputObjectValidatorTests.fs @@ -144,7 +144,7 @@ let ``Execute handles validation of invalid inline input records with all fields | RequestError [ zipCodeError ; addressError ] -> zipCodeError |> ensureInputObjectValidationError (Argument "record") "ZipCode must be 5 characters for US" [] "InputRecord!" addressError |> ensureInputObjectValidationError (Argument "recordNested") "HomeAddress and MailingAddress must be different" [] "InputRecordNested" - | response -> fail $"Expected RequestError GQLResponse but got {Environment.NewLine}{response}" + | response -> fail $"Expected 'RequestError' GQLResponse but got\n{response}" let variablesWithAllInputs (record, record1, record2, record3) = From bf6ab98edc6feeb561749cfb2976442ed6c2412d Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Thu, 21 Mar 2024 21:54:52 +0100 Subject: [PATCH 097/100] ...WebsocketMiddleware.fs: improved warning logging Co-authored-by: Andrii Chebukin --- .../GraphQLWebsocketMiddleware.fs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 9c867313a..512d9ae08 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -201,9 +201,7 @@ type GraphQLWebSocketMiddleware<'Root> () | Direct (data, _) -> do! data |> sendOutput id | RequestError problemDetails -> - logger.LogWarning( - "Request error: %s", - (String.Join ('\n', problemDetails |> Seq.map (fun x -> $"- %s{x.Message}"))) + logger.LogWarning("Request errors:\n{errors}", problemDetails) ) } From 4b16c87412caf46567503e0f2a049e069b9c4583 Mon Sep 17 00:00:00 2001 From: valber Date: Wed, 20 Mar 2024 23:44:22 +0100 Subject: [PATCH 098/100] .WebsocketMiddleware: better WS message deserialization exception handl. According to suggestion during code review. --- .../GraphQLWebsocketMiddleware.fs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 512d9ae08..723e52a79 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -45,18 +45,23 @@ type GraphQLWebSocketMiddleware<'Root> } let deserializeClientMessage (serializerOptions : JsonSerializerOptions) (msg : string) = taskResult { + let invalidJsonInClientMessageError() = + Result.Error <| InvalidMessage (4400, "invalid json in client message") try return JsonSerializer.Deserialize (msg, serializerOptions) with - | :? InvalidWebsocketMessageException as e -> return! Result.Error <| InvalidMessage (4400, e.Message.ToString ()) - | :? JsonException as e -> - if logger.IsEnabled (LogLevel.Debug) then - logger.LogDebug (e.ToString ()) - else - () - return! - Result.Error - <| InvalidMessage (4400, "Invalid JSON in the client message") + | :? InvalidWebsocketMessageException as ex -> + logger.LogError(ex, $"Invalid websocket message:{Environment.NewLine}{{payload}}", msg) + return! Result.Error <| InvalidMessage (4400, ex.Message.ToString ()) + | :? JsonException as ex when logger.IsEnabled(LogLevel.Trace) -> + logger.LogError(ex, $"Cannot deserialize WebSocket message:{Environment.NewLine}{{payload}}", msg) + return! invalidJsonInClientMessageError() + | :? JsonException as ex -> + logger.LogError(ex, "Cannot deserialize WebSocket message") + return! invalidJsonInClientMessageError() + | ex -> + logger.LogError(ex, "Unexpected exception \"{exceptionname}\" in GraphQLWebsocketMiddleware.", (ex.GetType().Name)) + return! invalidJsonInClientMessageError() } let isSocketOpen (theSocket : WebSocket) = @@ -202,7 +207,7 @@ type GraphQLWebSocketMiddleware<'Root> | Direct (data, _) -> do! data |> sendOutput id | RequestError problemDetails -> logger.LogWarning("Request errors:\n{errors}", problemDetails) - ) + } let logMsgReceivedWithOptionalPayload optionalPayload (msgAsStr : string) = From e7cfeff2ca3bb550d4140282e913966ab63f8bb6 Mon Sep 17 00:00:00 2001 From: valber Date: Thu, 21 Mar 2024 23:03:35 +0100 Subject: [PATCH 099/100] chat-app: naming ObjectDef variables according to implicit convention As requested during code review. --- samples/chat-app/server/Schema.fs | 72 +++++++++++++++---------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/samples/chat-app/server/Schema.fs b/samples/chat-app/server/Schema.fs index 53c45b900..52ff5bc95 100644 --- a/samples/chat-app/server/Schema.fs +++ b/samples/chat-app/server/Schema.fs @@ -137,7 +137,7 @@ module Schema = let chatRoomEvents_subscription_name = "chatRoomEvents" - let memberRoleInChatEnumDef = + let MemberRoleInChatEnumType = Define.Enum ( name = nameof MemberRoleInChat, options = [ @@ -146,7 +146,7 @@ module Schema = ] ) - let memberDef = + let MemberType = Define.Object ( name = nameof Member, description = "An organization member", @@ -165,7 +165,7 @@ module Schema = ] ) - let meAsAMemberDef = + let MeAsAMemberType = Define.Object ( name = nameof MeAsAMember, description = "An organization member", @@ -192,7 +192,7 @@ module Schema = ] ) - let chatMemberDef = + let ChatMemberType = Define.Object ( name = nameof ChatMember, description = "A chat member is an organization member participating in a chat room", @@ -208,11 +208,11 @@ module Schema = | MemberId theId -> theId ) Define.Field ("name", StringType, "the member's name", (fun _ (x : ChatMember) -> x.Name)) - Define.Field ("role", memberRoleInChatEnumDef, "the member's role in the chat", (fun _ (x : ChatMember) -> x.Role)) + Define.Field ("role", MemberRoleInChatEnumType, "the member's role in the chat", (fun _ (x : ChatMember) -> x.Role)) ] ) - let meAsAChatMemberDef = + let MeAsAChatMemberType = Define.Object ( name = nameof MeAsAChatMember, description = "A chat member is an organization member participating in a chat room", @@ -236,11 +236,11 @@ module Schema = | MemberId theId -> theId ) Define.Field ("name", StringType, "the member's name", (fun _ (x : MeAsAChatMember) -> x.Name)) - Define.Field ("role", memberRoleInChatEnumDef, "the member's role in the chat", (fun _ (x : MeAsAChatMember) -> x.Role)) + Define.Field ("role", MemberRoleInChatEnumType, "the member's role in the chat", (fun _ (x : MeAsAChatMember) -> x.Role)) ] ) - let chatRoomStatsDef = + let ChatRoomStatsType = Define.Object ( name = nameof ChatRoom, description = "A chat room as viewed from the outside", @@ -256,11 +256,11 @@ module Schema = | ChatRoomId theId -> theId ) Define.Field ("name", StringType, "the chat room's name", (fun _ (x : ChatRoom) -> x.Name)) - Define.Field ("members", ListOf chatMemberDef, "the members in the chat room", (fun _ (x : ChatRoom) -> x.Members)) + Define.Field ("members", ListOf ChatMemberType, "the members in the chat room", (fun _ (x : ChatRoom) -> x.Members)) ] ) - let chatRoomDetailsDef = + let ChatRoomDetailsType = Define.Object ( name = nameof ChatRoomForMember, description = "A chat room as viewed by a chat room member", @@ -278,20 +278,20 @@ module Schema = Define.Field ("name", StringType, "the chat room's name", (fun _ (x : ChatRoomForMember) -> x.Name)) Define.Field ( "meAsAChatMember", - meAsAChatMemberDef, + MeAsAChatMemberType, "the chat member that queried the details", fun _ (x : ChatRoomForMember) -> x.MeAsAChatMember ) Define.Field ( "otherChatMembers", - ListOf chatMemberDef, + ListOf ChatMemberType, "the chat members excluding the one who queried the details", fun _ (x : ChatRoomForMember) -> x.OtherChatMembers ) ] ) - let organizationStatsDef = + let OrganizationStatsType = Define.Object ( name = nameof Organization, description = "An organization as seen from the outside", @@ -307,12 +307,12 @@ module Schema = | OrganizationId theId -> theId ) Define.Field ("name", StringType, "the organization's name", (fun _ (x : Organization) -> x.Name)) - Define.Field ("members", ListOf memberDef, "members of this organization", (fun _ (x : Organization) -> x.Members)) - Define.Field ("chatRooms", ListOf chatRoomStatsDef, "chat rooms in this organization", (fun _ (x : Organization) -> x.ChatRooms)) + Define.Field ("members", ListOf MemberType, "members of this organization", (fun _ (x : Organization) -> x.Members)) + Define.Field ("chatRooms", ListOf ChatRoomStatsType, "chat rooms in this organization", (fun _ (x : Organization) -> x.ChatRooms)) ] ) - let organizationDetailsDef = + let OrganizationDetailsType = Define.Object ( name = nameof OrganizationForMember, description = "An organization as seen by one of the organization's members", @@ -330,26 +330,26 @@ module Schema = Define.Field ("name", StringType, "the organization's name", (fun _ (x : OrganizationForMember) -> x.Name)) Define.Field ( "meAsAMember", - meAsAMemberDef, + MeAsAMemberType, "the member that queried the details", fun _ (x : OrganizationForMember) -> x.MeAsAMember ) Define.Field ( "otherMembers", - ListOf memberDef, + ListOf MemberType, "members of this organization", fun _ (x : OrganizationForMember) -> x.OtherMembers ) Define.Field ( "chatRooms", - ListOf chatRoomStatsDef, + ListOf ChatRoomStatsType, "chat rooms in this organization", fun _ (x : OrganizationForMember) -> x.ChatRooms ) ] ) - let aChatRoomMessageDef description name = + let aChatRoomMessageTypeWith description name = Define.Object ( name = name, description = description, @@ -440,10 +440,10 @@ module Schema = let newMessageDef = nameof NewMessage - |> aChatRoomMessageDef "a new public message has been sent in the chat room" + |> aChatRoomMessageTypeWith "a new public message has been sent in the chat room" let editedMessageDef = nameof EditedMessage - |> aChatRoomMessageDef "a public message of the chat room has been edited" + |> aChatRoomMessageTypeWith "a public message of the chat room has been edited" let deletedMessageDef = nameof DeletedMessage |> aChatRoomEventForMessageId "a public message of the chat room has been deleted" @@ -454,7 +454,7 @@ module Schema = nameof MemberLeft |> aChatRoomEventForMemberIdAndName "a member has left the chat" - let chatRoomSpecificEventDef = + let ChatRoomSpecificEventType = Define.Union ( name = nameof ChatRoomSpecificEvent, options = [ newMessageDef; editedMessageDef; deletedMessageDef; memberJoinedDef; memberLeftDef ], @@ -477,7 +477,7 @@ module Schema = description = "data which is specific to a certain type of event" ) - let chatRoomEventDef = + let ChatRoomEventType = Define.Object ( name = nameof ChatRoomEvent, description = "Something that happened in the chat room, like a new message sent", @@ -500,20 +500,20 @@ module Schema = ) Define.Field ( "specificData", - chatRoomSpecificEventDef, + ChatRoomSpecificEventType, "the event's specific data", fun _ (x : ChatRoomEvent) -> x.SpecificData ) ]) ) - let query = + let QueryType = Define.Object ( name = "Query", fields = [ Define.Field ( "organizations", - ListOf organizationStatsDef, + ListOf OrganizationStatsType, "gets all available organizations", fun _ _ -> FakePersistence.Organizations.Values @@ -533,13 +533,13 @@ module Schema = } |> schemaConfig.SubscriptionProvider.Publish chatRoomEvents_subscription_name - let mutation = + let MutationType = Define.Object ( name = "Mutation", fields = [ Define.Field ( "enterOrganization", - organizationDetailsDef, + OrganizationDetailsType, "makes a new member enter an organization", [ Define.Input ("organizationId", GuidType, description = "the ID of the organization") @@ -587,7 +587,7 @@ module Schema = ) Define.Field ( "createChatRoom", - chatRoomDetailsDef, + ChatRoomDetailsType, "creates a new chat room for a user", [ Define.Input ("organizationId", GuidType, description = "the ID of the organization in which the chat room will be created") @@ -624,7 +624,7 @@ module Schema = ) Define.Field ( "enterChatRoom", - chatRoomDetailsDef, + ChatRoomDetailsType, "makes a member enter a chat room", [ Define.Input ("organizationId", GuidType, description = "the ID of the organization the chat room and member are in") @@ -826,7 +826,7 @@ module Schema = ] ) - let rootDef = + let RootType = Define.Object ( name = "Root", description = "contains general request information", @@ -837,14 +837,14 @@ module Schema = ] ) - let subscription = + let SubscriptionType = Define.SubscriptionObject ( name = "Subscription", fields = [ Define.SubscriptionField ( chatRoomEvents_subscription_name, - rootDef, - chatRoomEventDef, + RootType, + ChatRoomEventType, "events related to a specific chat room", [ Define.Input ("chatRoomId", GuidType, description = "the ID of the chat room to listen to events from") @@ -872,6 +872,6 @@ module Schema = ] ) - let schema : ISchema = Schema (query, mutation, subscription, schemaConfig) + let schema : ISchema = Schema (QueryType, MutationType, SubscriptionType, schemaConfig) let executor = Executor (schema, []) From f5f52940446efb415d4f993f44330f67cc5e7eb9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 22 Mar 2024 18:42:59 +0400 Subject: [PATCH 100/100] Made error response static and fixed capital in error --- .../GraphQLWebsocketMiddleware.fs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 723e52a79..6713725a7 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -44,24 +44,25 @@ type GraphQLWebSocketMiddleware<'Root> return JsonSerializer.Serialize (raw, jsonSerializerOptions) } + static let invalidJsonInClientMessageError = + Result.Error <| InvalidMessage (4400, "Invalid json in client message") + let deserializeClientMessage (serializerOptions : JsonSerializerOptions) (msg : string) = taskResult { - let invalidJsonInClientMessageError() = - Result.Error <| InvalidMessage (4400, "invalid json in client message") try return JsonSerializer.Deserialize (msg, serializerOptions) with | :? InvalidWebsocketMessageException as ex -> - logger.LogError(ex, $"Invalid websocket message:{Environment.NewLine}{{payload}}", msg) + logger.LogError(ex, "Invalid websocket message:\n{payload}", msg) return! Result.Error <| InvalidMessage (4400, ex.Message.ToString ()) | :? JsonException as ex when logger.IsEnabled(LogLevel.Trace) -> - logger.LogError(ex, $"Cannot deserialize WebSocket message:{Environment.NewLine}{{payload}}", msg) - return! invalidJsonInClientMessageError() + logger.LogError(ex, "Cannot deserialize WebSocket message:\n{payload}", msg) + return! invalidJsonInClientMessageError | :? JsonException as ex -> logger.LogError(ex, "Cannot deserialize WebSocket message") - return! invalidJsonInClientMessageError() + return! invalidJsonInClientMessageError | ex -> - logger.LogError(ex, "Unexpected exception \"{exceptionname}\" in GraphQLWebsocketMiddleware.", (ex.GetType().Name)) - return! invalidJsonInClientMessageError() + logger.LogError(ex, $"Unexpected exception '{ex.GetType().Name}' in GraphQLWebsocketMiddleware") + return! invalidJsonInClientMessageError } let isSocketOpen (theSocket : WebSocket) =