Skip to content

DefineRec.InputObject with fixFields? #596

Description

@njlr

For recursive input types it's nice not to have warnings.

I came up with this helper function:

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

Minimal usage:

type Comment =
  {
    Message : string
    Reply : Comment
  }

DefineRec.InputObject<Comment>(
  name = "Comment",
  fixFields =
    fun self ->
      [
        Define.Input("message", StringType)
        Define.Input("reply", self)
      ]
)

Is this a good approach?

Full demo usage
#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"
    }
  ]
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions