Discussion: Stronger typing for arguments in resolvers?
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 30/100
- Loại issue
- Tính năng
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Đình trệ
- Công nghệ
- graphql
- Lĩnh vực
- api, backend-api-design
Hướng nghiên cứu
Bắt đầu bằng cách xem xét các API Args, InputFieldDef, Define.Field và Define.AsyncField hiện có, sau đó so sánh chúng với các ví dụ Input và FieldDef.define được đề xuất. Công việc được coi là hoàn tất khi thư viện có một cách tiếp cận được thống nhất và an toàn về kiểu để truyền các kiểu đối số của resolver qua các định nghĩa schema, bao gồm cả các field đồng bộ và bất đồng bộ, đồng thời các ví dụ được đề xuất đã được xác thực.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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?
- Ngôn ngữ chính
- F#
- Star
- 406
- Fork
- 74
- Merge trung bình
- 1 ngày 8 giờ
- Pull request đã merge (30 ngày)
- 14
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của fsprojects/FSharp.Data.GraphQL
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 52/100
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 45/100
-
fsprojects/FSharp.Data.GraphQL#573 · 1 reaction · 2 người được giao ·
-
FR: Suave package Đang mở
fsprojects/FSharp.Data.GraphQL#566 · 1 bình luận · 1 reaction · 2 người được giao ·
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 30/100
Tất cả issue của fsprojects/FSharp.Data.GraphQL
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
speaches-ai/speaches#678 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
-
[BUG] ECR GetAuthorizationToken returns a proxyEndpoint for the default region, not the request's Đang mởbug ecr
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
conda-forge/spacy-feedstock#177 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100