Skip to content

Commit

Permalink
Relay Revamp (#484)
Browse files Browse the repository at this point in the history
* Rework Relay types to enable async fetching
* Add a relay sample that fetches data efficiently from SQLite
* Remove Suave
* Bump package version
* Fix StarWars sample
* Switch Relay to ValueOption
* Fixed `System.Text.RegularExpressions` reference vulnerability

---------

Co-authored-by: Andrii Chebukin <XperiAndri@Outlook.com>
  • Loading branch information
njlr and xperiandri authored Sep 1, 2024
1 parent 6f1dc93 commit 845f21b
Show file tree
Hide file tree
Showing 31 changed files with 985 additions and 5,665 deletions.
67 changes: 41 additions & 26 deletions FSharp.Data.GraphQL.sln
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "star-wars-api", "samples\st
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "chat-app", "samples\chat-app\server\chat-app.fsproj", "{225B0790-C6B6-425C-9093-F359A4C635D3}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "relay-book-store", "samples\relay-book-store\relay-book-store.fsproj", "{C24EB38E-326C-4770-BB20-9838694EE5E6}"
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}"
Expand Down Expand Up @@ -244,6 +246,30 @@ Global
{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
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|x64.ActiveCfg = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|x64.Build.0 = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|x86.ActiveCfg = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|x86.Build.0 = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|Any CPU.Build.0 = Release|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|x64.ActiveCfg = Release|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|x64.Build.0 = Release|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|x86.ActiveCfg = Release|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|x86.Build.0 = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|x64.ActiveCfg = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|x64.Build.0 = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|x86.ActiveCfg = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|x86.Build.0 = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|Any CPU.Build.0 = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|x64.ActiveCfg = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|x64.Build.0 = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|x86.ActiveCfg = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.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
Expand Down Expand Up @@ -316,30 +342,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
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|x64.ActiveCfg = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|x64.Build.0 = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|x86.ActiveCfg = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Debug|x86.Build.0 = Debug|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|Any CPU.Build.0 = Release|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|x64.ActiveCfg = Release|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|x64.Build.0 = Release|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|x86.ActiveCfg = Release|Any CPU
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80}.Release|x86.Build.0 = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|x64.ActiveCfg = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|x64.Build.0 = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|x86.ActiveCfg = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Debug|x86.Build.0 = Debug|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|Any CPU.Build.0 = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|x64.ActiveCfg = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|x64.Build.0 = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|x86.ActiveCfg = Release|Any CPU
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E}.Release|x86.Build.0 = Release|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Debug|x64.ActiveCfg = Debug|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Debug|x64.Build.0 = Debug|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Debug|x86.ActiveCfg = Debug|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Debug|x86.Build.0 = Debug|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Release|Any CPU.Build.0 = Release|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Release|x64.ActiveCfg = Release|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Release|x64.Build.0 = Release|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Release|x86.ActiveCfg = Release|Any CPU
{C24EB38E-326C-4770-BB20-9838694EE5E6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -352,6 +366,8 @@ Global
{6768EA38-1335-4B8E-BC09-CCDED1F9AAF6} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78}
{474179D3-0090-49E9-88F8-2971C0966077} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78}
{554A6833-1E72-41B4-AAC1-C19371EC061B} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78}
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78}
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78}
{E011A3B2-3D96-48E3-AF5F-DA544FF5C5FE} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78}
{8FB23F61-77CB-42C7-8EEC-B22D7C4E4067} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78}
{B075CD55-CEA4-4C30-A088-48319AADF070} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78}
Expand All @@ -369,8 +385,7 @@ 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}
{0A83D100-DE1D-49A5-B6F7-CC78C7A15F80} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78}
{7873CD7F-BCDD-47AE-9743-5D033DA84E3E} = {BEFD8748-2467-45F9-A4AD-B450B12D5F78}
{C24EB38E-326C-4770-BB20-9838694EE5E6} = {B0C25450-74BF-40C2-9E02-09AADBAE2C2F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C5B9895C-9DF8-4557-8D44-7D0C4C31F86E}
Expand Down
6 changes: 6 additions & 0 deletions Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<PackageReference Update="System.Management" Version="6.*" />
<PackageReference Update="System.Reactive" Version="6.*" NoWarn="NU1608" />
<PackageReference Update="System.Text.Json" Version="$(SystemVersion)" />
<PackageReference Update="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
<ItemGroup Label="Build">
<PackageReference Update="BlackFox.VsWhere" Version="1.1.0" />
Expand Down Expand Up @@ -67,8 +68,10 @@
</ItemGroup>
<ItemGroup Label="Tests and Samples">
<PackageReference Update="CommandLineParser" Version="2.9.*" />
<PackageReference Update="Donald" Version="10.1.0" />
<PackageReference Update="EntityFramework" Version="1.*" />
<PackageReference Update="FSharp.Data.TypeProviders" Version="1.*" />
<PackageReference Update="FsToolkit.ErrorHandling" Version="4.15.3" />
<PackageReference Update="Giraffe" Version="6.*" />
<PackageReference Update="GraphQL.Server.Ui.Altair" Version="7.*" />
<PackageReference Update="GraphQL.Server.Ui.GraphiQL" Version="7.*" />
Expand All @@ -77,11 +80,14 @@
<PackageReference Update="HotChocolate.AspNetCore" Version="13.*" />
<PackageReference Update="Iced" Version="1.17.*" />
<PackageReference Update="Microsoft.CodeCoverage" Version="17.3.*" />
<PackageReference Update="Microsoft.Data.Sqlite" Version="8.0.6" />
<PackageReference Update="Microsoft.Diagnostics.NETCore.Client" Version="0.2.*" />
<PackageReference Update="Microsoft.Diagnostics.Runtime" Version="2.2.*" />
<PackageReference Update="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.*" />
<PackageReference Update="Microsoft.NETCore.Platforms" Version="6.0.*" />
<PackageReference Update="Newtonsoft.Json" Version="13.*" />
<PackageReference Update="System.Data.Common" Version="4.3.0" />
<PackageReference Update="Thoth.Json.Net" Version="12.0.0" />
<PackageReference Update="Validus" Version="4.1.*" />
</ItemGroup>
</Project>
16 changes: 4 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type Person =
{ FirstName: string
LastName: string }
// Define GraphQL type
// Define GraphQL type
let PersonType = Define.Object(
name = "Person",
fields = [
Expand All @@ -45,7 +45,7 @@ let executor = Executor(schema)
// Retrieve person data
let johnSnow = { FirstName = "John"; LastName = "Snow" }
let reply = executor.AsyncExecute(Parser.parse "{ firstName, lastName }", johnSnow) |> Async.RunSynchronously
// #> { data: { "firstName", "John", "lastName", "Snow" } }
// #> { data: { "firstName", "John", "lastName", "Snow" } }
```

It's type safe. Things like invalid fields or invalid return types will be checked at compile time.
Expand Down Expand Up @@ -79,14 +79,6 @@ Go to the [GraphiQL sample directory](https://github.com/bazingatechnologies/FSh
}
```

### Relay.js starter kit

A [second sample](https://github.com/bazingatechnologies/FSharp.Data.GraphQL/tree/dev/samples/relay-starter-kit) is a F#-backed version of of popular Relay Starter Kit - an example application using React.js + Relay with Relay-compatible server API.

To run it, build `FSharp.Data.GraphQL` and `FSharp.Data.GraphQL.Relay` projects using Debug settings. Then start server by running `server.fsx` script in your FSI - this will start a relay-compatible F# server on port 8083. Then build node.js frontend by getting all dependencies (`npm i`) and running it (`npm run serve | npm run dev`) - this will start webpack server running React application using Relay for managing application state. You can visit it on [http://localhost:8083/](http://localhost:8083/) .

In order to update client schema, visit [http://localhost:8083/](http://localhost:8083/) and copy-paste the response (which is the introspection query result from the current F# server) into *data/schema.json*.

## Stream features

The `stream` directive now has additional features, like batching (buffering) by interval and/or batch size. To make it work, a custom stream directive must be placed inside the `SchemaConfig.Directives` list, this custom directive containing two optional arguments called `interval` and `preferredBatchSize`:
Expand Down Expand Up @@ -192,7 +184,7 @@ type MyProvider = GraphQLProvider<"swapi_schema.json">
From now on, you can start running queries and mutations:

```fsharp
let operation =
let operation =
MyProvider.Operation<"""query q {
hero (id: "1001") {
name
Expand Down Expand Up @@ -393,7 +385,7 @@ And the value recovered by the filter in the query is usable in the `ResolveFiel

```fsharp
Define.Field("friends", ListOf (Nullable CharacterType),
resolve = fun ctx (d : Droid) ->
resolve = fun ctx (d : Droid) ->
ctx.Filter |> printfn "Droid friends filter: %A"
d.Friends |> List.map getCharacter |> List.toSeq)
```
Expand Down
97 changes: 97 additions & 0 deletions samples/relay-book-store/DB.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
module FSharp.Data.GraphQL.Samples.RelayBookStore.DB

open System.Data
open System.Data.Common
open Donald

let fetchBooksTotalCount (db : IDbConnection) = async {
let sql = "SELECT COUNT(id) AS n FROM books"

let! ct = Async.CancellationToken

let! count =
db
|> Db.newCommand sql
|> Db.setCancellationToken ct
|> Db.Async.querySingle (fun read -> read.ReadInt32 ("n"))
|> Async.AwaitTask

return Option.get count
}

let private readBook (read : DbDataReader) : Book = {
ID = read.ReadString ("id")
Title = read.ReadString ("title")
Year = read.ReadInt32 ("year")
}

let tryFetchBook (id : string) (db : IDbConnection) = async {
let sql = "SELECT * FROM books WHERE id = @id LIMIT 1"

let parameters = [ "id", SqlType.String id ]

let! ct = Async.CancellationToken

let! maybeBook =
db
|> Db.newCommand sql
|> Db.setParams parameters
|> Db.setCancellationToken ct
|> Db.Async.querySingle readBook
|> Async.AwaitTask

return maybeBook
}

let fetchBooksPage (maybeCursor : BookCursor voption) (isCursorInclusive : bool) (isForward : bool) (limit : int) (db : IDbConnection) =
if limit < 0 then
invalidArg (nameof limit) "must be non-negative"

async {
let whereClause =
match maybeCursor with
| ValueSome _ ->
if isForward then
if isCursorInclusive then
"WHERE title > @cursor_title OR (title = @cursor_title AND id >= @cursor_id)"
else
"WHERE title > @cursor_title OR (title = @cursor_title AND id > @cursor_id)"
else if isCursorInclusive then
"WHERE title < @cursor_title OR (title = @cursor_title AND id <= @cursor_id)"
else
"WHERE title < @cursor_title OR (title = @cursor_title AND id < @cursor_id)"
| ValueNone -> ""

let orderByClause =
if isForward then
"ORDER BY title ASC, id ASC"
else
"ORDER BY title DESC, id DESC"

let sql =
$"""SELECT *
FROM books
%s{whereClause}
%s{orderByClause}
LIMIT %i{limit}"""

let parameters = [
match maybeCursor with
| ValueSome cursor ->
"cursor_id", SqlType.String cursor.ID
"cursor_title", SqlType.String cursor.Title
| ValueNone -> ()
]

let! ct = Async.CancellationToken

let! records =
db
|> Db.newCommand sql
|> Db.setParams parameters
|> Db.setCancellationToken ct
|> Db.Async.query readBook
|> Async.AwaitTask

return records
}
35 changes: 35 additions & 0 deletions samples/relay-book-store/Domain.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace FSharp.Data.GraphQL.Samples.RelayBookStore

type Book = { ID : string; Title : string; Year : int }

type BookCursor = { ID : string; Title : string }

[<RequireQualifiedAccess>]
module BookCursor =

open FsToolkit.ErrorHandling
open Thoth.Json.Net

let ofBook (x : Book) : BookCursor = { ID = x.ID; Title = x.Title }

let private encoder = fun x -> Encode.object [ "i", Encode.string x.ID; "t", Encode.string x.Title ]

let private decoder =
Decode.object (fun get -> {
ID = get.Required.Field "i" Decode.string
Title = get.Required.Field "t" Decode.string
})

let tryDecode (x : string) : BookCursor option = option {
let! bytes = Base64.tryDecode x
let! json = Utf8.tryDecode bytes

return! Decode.fromString decoder json |> Result.toOption
}

let encode (x : BookCursor) : string =
x
|> encoder
|> Encode.toString 0
|> Utf8.encode
|> Base64.encode
37 changes: 37 additions & 0 deletions samples/relay-book-store/GenerateDB.fsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#r "nuget: Microsoft.Data.Sqlite, 8.0.6"
#r "nuget: Donald, 10.1.0"

open Microsoft.Data.Sqlite
open Donald

let slugify (x : string) = x.Replace(' ', '-').Replace(''', '-').ToLowerInvariant ()

let books = [
"Accelerando", 2005
"Consider Phlebas", 1987
"Dune", 1965
"Ender's Game", 1985
"Gateway", 1977
"Interface", 1994
"Jurrasic Park", 1990
"Roadside Picnic", 1972
"Stand on Zanzibar", 1968
"The Sheep Look Up", 1972
"The Mountain Trail and its Message", 1997
"We", 1924
]

let db = new SqliteConnection ("Data Source=app.db")

db
|> Db.newCommand "CREATE TABLE books (id PRIMARY KEY, title, year); "
|> Db.exec

db
|> Db.newCommand "INSERT INTO books (id, title, year) VALUES (@id, @title, @year)"
|> Db.execMany [
for book in books do
let title, year = book
let id = slugify title
[ "id", SqlType.String id; "title", SqlType.String title; "year", SqlType.Int year ]
]
Loading

0 comments on commit 845f21b

Please sign in to comment.