Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Discussion: Stronger typing for arguments in resolvers?

Đang mở
#367 10 bình luận 0 reaction 0 người được giao Xem trên GitHub

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:

  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?

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

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. 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.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của fsprojects/FSharp.Data.GraphQL

Tất cả issue của fsprojects/FSharp.Data.GraphQL

Issue tương tự

Thêm issue về Backend & API Design

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.