Hacktoberfest 2026: as issues que os mantenedores marcaram para outubro, abertas e boas para iniciantes. Ver issues do Hacktoberfest

Discussion: Stronger typing for arguments in resolvers?

Aberta
#367 10 comentários 0 reações 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 1 dia

Ninguém assumiu esta issue ainda.

Avaliação

Dificuldade
5/5
Tempo estimado
Mais de uma semana
Facilidade para iniciantes
30/100
Tipo de issue
Funcionalidade
Clareza
Razoavelmente clara
Status de atividade
Estagnada
Stack de tecnologia
graphql

Direção de pesquisa

Comece revisando as APIs existentes Args, InputFieldDef, Define.Field e Define.AsyncField e, em seguida, compare-as com os exemplos propostos de Input e FieldDef.define. O trabalho estará concluído quando a biblioteca tiver uma abordagem acordada e type-safe para transportar os tipos dos argumentos do resolver pelas definições de schema, incluindo campos síncronos e assíncronos, e os exemplos propostos estiverem validados.

Escrita pelo modelo de indexação a partir do texto da issue.

Descrição

When writing resolver functions, my most common run-time errors are caused by unpacking the arguments incorrectly. This is because the API presents a Map<string, obj>, so it's easy to get the casting wrong!

I realized that when designing the schema, we actually have all of the type information already. The issue is that it's not passed to the resolve function!

Scroll down for a small example of the issue.

So, I set out to design an approach that carries the types from the argument list to the resolve function.

The idea is to:

  1. Provide type-safe building blocks for the built-in GraphQL types
  2. Provide functions for composing these building blocks as the user requires

Here is the main type definition:

type Args = Map<string, obj>

type Input<'t> =
  {
    InputFieldDefs : InputFieldDef list
    Extract : Args -> 't
  }

It wraps the type we already have (InputFieldDef) but adds a strongly-typed function for extracting the value during the resolve stage.

From this, we can create building blocks for built-in GraphQL types. For example, here is Int:

module Input =

  let int (name : string) (defaultValue : int option) (description : string option) : Input<int> =
    {
      InputFieldDefs =
        [
          Define.Input(name, Int, ?defaultValue=defaultValue, ?description=description)
        ]
      Extract =
        fun args ->
          match args |> Map.tryFind name with
          | Some o ->
            match o with
            | :? int as i -> i
            | _ -> failwith $"Argument \"{name}\" was not an int, it was a {o.GetType().Name}"
          | None ->
            match defaultValue with
            | Some i -> i
            | None -> failwith $"Argument \"{name}\" not found"
    }

We can combine two Input<_> objects together like this:

  let zip (a : Input<'a>) (b : Input<'b>) : Input<'a * 'b> =
    {
      InputFieldDefs = a.InputFieldDefs @ b.InputFieldDefs
      Extract =
        fun args ->
          let x = a.Extract args
          let y = b.Extract args
          x, y
    }

For example, if the resolve function expects an int and a string we can do:

Input.zip
  (Input.int "n" None None)
  (Input.string "s" None None)

And we can build a Computation Expression version of this too!

input {
  let! n = Input.int "n" None None
  and! s = Input.string "s" None None

  return n, s
}

The next step is to provide a function for creating a FieldDef that takes an Input<'t>:

module FieldDef =

  let define (name : string) (typeDef : #OutputDef<'t>) (input : Input<'arg>) resolve =
    Define.Field(
      name,
      typeDef,
      input.InputFieldDefs,
      (fun (ctx : ResolveFieldContext) x ->
        let arg = input.Extract ctx.Args

        resolve arg ctx x))

Here the resolve function takes 3 arguments instead of the usual 2. The first is the strongly-typed argument value.

And an Async version:

  let defineAsync(name : string) (typeDef : #OutputDef<'t>) (description : string) (input : Input<'arg>) resolve =
    Define.AsyncField(
      name,
      typeDef,
      description,
      input.InputFieldDefs,
      (fun (ctx : ResolveFieldContext) x ->
        async {
          let arg = input.Extract ctx.Args

          return! resolve arg ctx x
        }))

Here is a small schema that demonstrates how it all fits together:

open System

type ToDoItem =
  {
    ID : Guid
    Created : DateTime
    Title : string
    IsDone : bool
  }

type Root () =
  let mutable toDoItems = Map.empty

  member this.TryFetchToDoItem(id : Guid) =
    async {
      return Map.tryFind id toDoItems
    }

  member this.FetchToDoItems() =
    async {
      return
        toDoItems
        |> Map.toSeq
        |> Seq.map snd
        |> Seq.sortBy (fun x -> x.Created, x.ID)
        |> Seq.toList
    }

  member this.CreateToDoItem(title : string) =
    async {
      let toDoItem =
        {
          ID = Guid.NewGuid()
          Created = DateTime.UtcNow
          IsDone = false
          Title = title
        }

      toDoItems <- Map.add toDoItem.ID toDoItem toDoItems

      return toDoItem
    }

  member this.TryUpdateToDoItem(id : Guid, ?title : string, ?isDone : bool) =
    async {
      match Map.tryFind id toDoItems with
      | Some toDoItem ->
        let nextToDoItem =
          {
            toDoItem with
              Title = title |> Option.defaultValue toDoItem.Title
              IsDone = isDone |> Option.defaultValue toDoItem.IsDone
          }

        if toDoItem <> nextToDoItem then
          toDoItems <- Map.add id nextToDoItem toDoItems

        return Some nextToDoItem
      | None ->
        return None
    }

open FSharp.Data.GraphQL.Execution
open FSharp.Data.GraphQL.Types.SchemaDefinitions

let toDoItemType =
  Define.Object<ToDoItem>(
    "ToDoItem",
    [
      Define.Field("id", Guid, fun ctx x -> x.ID)
      Define.Field("created", String, fun ctx x -> x.Created.ToString("o"))
      Define.Field("title", String, fun ctx x -> x.Title)
      Define.Field("isDone", Boolean, fun ctx x -> x.IsDone)
    ]
  )

let queryType =
  Define.Object<Root>(
    "Query",
    [
      FieldDef.defineAsync
        "toDoItem"
        (Nullable toDoItemType)
        "Fetches a single to-do item"
        (Input.guid "id" None None)
        (fun g ctx (root : Root) ->
          root.TryFetchToDoItem(g))

      Define.AsyncField(
        "toDoItems",
        ListOf toDoItemType,
        "Fetches all to-do items",
        fun ctx (root : Root) ->
          root.FetchToDoItems())
    ]
  )

let mutationType =
  Define.Object<Root>(
    "Mutation",
    [
      FieldDef.defineAsync
        "createToDoItem"
        toDoItemType
        "Creates a new to-do item"
        (Input.string "title" None None)
        (fun title ctx (root : Root) ->
          root.CreateToDoItem(title))

      FieldDef.defineAsync
        "updateToDoItem"
        (Nullable toDoItemType)
        "Updates a to-do item"
        (input {
          let! id = Input.guid "id" None None
          and! title = Input.nullableString "title" None None
          and! isDone = Input.nullableBool "isDone" None None

          return id, title, isDone
        })
        (fun args ctx (root : Root) ->
          async {
            let id, title, isDone = args

            return! root.TryUpdateToDoItem(id, ?title=title, ?isDone=isDone)
          })
    ]
  )

let schema = Schema(queryType, mutationType)

I will attach a complete demo script that can run in FSI.

What does everyone think?
Could we build this into the library?

Linguagem predominante
F#
Estrelas
407
Forks
74
Merge médio
15h 59min
PRs com merge (30d)
20

Preparar o ambiente

Este projeto não oferece contêiner de desenvolvimento, Dockerfile nem guia de contribuição, então a configuração fica por sua conta: comece pelo README e veja nosso guia da primeira contribuição para os passos gerais.

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de fsprojects/FSharp.Data.GraphQL

Todas as issues de fsprojects/FSharp.Data.GraphQL

Issues semelhantes

Mais issues de Backend & API Design

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.