Discussion: Stronger typing for arguments in resolvers?
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
- Domínio
- api, backend-api-design
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:
- Provide type-safe building blocks for the built-in GraphQL types
- 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
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de fsprojects/FSharp.Data.GraphQL
-
Introspection emits JSON-serialized value as `defaultValue` instead of a GraphQL literal (breaks enum defaults)Talvez já em andamento @xperiandri assumiu há 9 dias. Aberta
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 65/100
fsprojects/FSharp.Data.GraphQL#623 ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 52/100
fsprojects/FSharp.Data.GraphQL#613 ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 3/5 1-2 dias Facilidade para iniciantes 45/100
fsprojects/FSharp.Data.GraphQL#596 ·
Mantenedores costumam responder em até 1 dia
-
`documentId` has different values for the same query contentTalvez já em andamento @xperiandri assumiu há 147 dias. Aberta
fsprojects/FSharp.Data.GraphQL#573 · 1 reação · 2 responsáveis ·
Mantenedores costumam responder em até 1 dia
-
FR: Suave packageTalvez já em andamento @xperiandri assumiu há 147 dias. Aberta
fsprojects/FSharp.Data.GraphQL#566 · 1 comentário · 1 reação · 2 responsáveis ·
Mantenedores costumam responder em até 1 dia
Todas as issues de fsprojects/FSharp.Data.GraphQL
Issues semelhantes
-
[Anon] Imperium - Adeptus Astartes - Dark Angels: Aggressors Autostorm Bolter Gauntlets IncorrectAberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 65/100
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 77/100
GreedyBear-Project/GreedyBear#1727 ·
Mantenedores costumam responder em até 2 dias
-
bug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 66/100
modusensus/dsh-mneme#411 ·
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 84/100
ARM-software/CMSIS-DSP#354 ·
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 77/100
Mantenedores costumam responder em até 1 dia