#r "nuget: FSharp.Data.GraphQL.Server, 3.1.1"
open System.Text.Json
open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Types
type DefineRec with
static member InputObject(
name: string,
fixFields: InputObjectDefinition<'a> -> InputFieldDef list,
?description: string
) : InputObjectDefinition<'a> =
let mutable self = Unchecked.defaultof<InputObjectDefinition<'a>>
let definition =
{
Name = name
Fields = lazy (fixFields self |> List.toArray)
Description = description
Validator = Validation.GQLValidator.empty
ExecuteInput = Unchecked.defaultof<_>
}
self <- definition
definition
type StringFilter =
{
Eq : string option
Ne : string option
In : string list option
Nin : string list option
}
type ProductFilter =
{
Title : StringFilter option
Category : StringFilter option
Brand : StringFilter option
And : ProductFilter list option
Or : ProductFilter list option
Not : ProductFilter option
}
type Product =
{
ID : int
Title : string
Category : string
Brand : string
}
let stringFilterInputType : InputObjectDefinition<StringFilter> =
Define.InputObject(
name = "StringFilterInput",
fields =
[
Define.Input("eq", Nullable StringType)
Define.Input("ne", Nullable StringType)
Define.Input("in", Nullable (ListOf StringType))
Define.Input("nin", Nullable (ListOf StringType))
]
)
let productFilterInputType : InputObjectDefinition<ProductFilter> =
DefineRec.InputObject(
name = "ProductFilterInput",
fixFields =
fun self ->
[
Define.Input("title", Nullable stringFilterInputType)
Define.Input("category", Nullable stringFilterInputType)
Define.Input("brand", Nullable stringFilterInputType)
Define.Input("and", Nullable (ListOf self))
Define.Input("or", Nullable (ListOf self))
Define.Input("not", Nullable self)
]
)
let productType =
Define.Object<Product>(
name = "Product",
fields =
[
Define.Field("id", IntType, fun _ p -> p.ID)
Define.Field("title", StringType, fun _ p -> p.Title)
Define.Field("category", StringType, fun _ p -> p.Category)
Define.Field("brand", StringType, fun _ p -> p.Brand)
]
)
let evalStringFilter (targetVal : string) (filter : StringFilter) : bool =
let matchEq = filter.Eq |> Option.forall (fun v -> targetVal = v)
let matchNe = filter.Ne |> Option.forall (fun v -> targetVal <> v)
let matchIn = filter.In |> Option.forall (fun list -> List.contains targetVal list)
let matchNin = filter.Nin |> Option.forall (fun list -> not (List.contains targetVal list))
matchEq && matchNe && matchIn && matchNin
let rec matchesProduct (product : Product) (filter : ProductFilter) : bool =
let titleMatch = filter.Title |> Option.forall (evalStringFilter product.Title)
let categoryMatch = filter.Category |> Option.forall (evalStringFilter product.Category)
let brandMatch = filter.Brand |> Option.forall (evalStringFilter product.Brand)
let fieldsValid = titleMatch && categoryMatch && brandMatch
let andValid = filter.And |> Option.forall (List.forall (matchesProduct product))
let orValid = filter.Or |> Option.forall (List.exists (matchesProduct product))
let notValid = filter.Not |> Option.forall (fun f -> not (matchesProduct product f))
fieldsValid && andValid && orValid && notValid
let products =
[
{ ID = 1; Title = "Laptop"; Category = "Electronics"; Brand = "BrandA" }
{ ID = 2; Title = "Smartphone"; Category = "Electronics"; Brand = "BrandB" }
{ ID = 3; Title = "Headphones"; Category = "Accessories"; Brand = "BrandA" }
{ ID = 4; Title = "Coffee Maker"; Category = "Home Appliances"; Brand = "BrandC" }
{ ID = 5; Title = "Blender"; Category = "Home Appliances"; Brand = "BrandD" }
]
let fetchProducts (filter : ProductFilter option) =
match filter with
| Some f -> List.filter (fun p -> matchesProduct p f) products
| None -> products
let queryType =
Define.Object(
name = "Query",
fields = [
Define.Field(
name = "products",
typedef = ListOf productType,
args =
[
Define.Input("filter", Nullable productFilterInputType)
],
resolve =
fun ctx () ->
let filterArg : ProductFilter voption = ctx.TryArg("filter")
fetchProducts (Option.ofValueOption filterArg)
)
]
)
let schema = Schema(queryType)
let executor = Executor(schema)
let query =
"""
query {
products(
filter: {
or: [
{ category: { eq: "Electronics" } },
{ brand: { eq: "BrandA" } }
]
}
) {
id
title
category
brand
}
}
"""
let response =
executor.AsyncExecute(
query,
(fun () ->
{
new IInputExecutionContext with
member this.GetFile(_ : string) =
Result.Error "Not implemented"
})
)
|> Async.RunSynchronously
let content =
match response.Content with
| GQLResponseContent.Direct (content, []) -> content
| GQLResponseContent.Direct (_, errors) -> failwith $"Unexpected errors: {errors}"
| x -> failwith $"Unexpected response content: %A{x}"
let json = System.Text.Json.JsonSerializer.Serialize(content, JsonSerializerOptions(WriteIndented = true))
printfn "%s" json
{
"products": [
{
"id": 1,
"title": "Laptop",
"category": "Electronics",
"brand": "BrandA"
},
{
"id": 2,
"title": "Smartphone",
"category": "Electronics",
"brand": "BrandB"
},
{
"id": 3,
"title": "Headphones",
"category": "Accessories",
"brand": "BrandA"
}
]
}
For recursive input types it's nice not to have warnings.
I came up with this helper function:
Minimal usage:
Is this a good approach?
Full demo usage
{ "products": [ { "id": 1, "title": "Laptop", "category": "Electronics", "brand": "BrandA" }, { "id": 2, "title": "Smartphone", "category": "Electronics", "brand": "BrandB" }, { "id": 3, "title": "Headphones", "category": "Accessories", "brand": "BrandA" } ] }