FSharp.Data.GraphQL.ProjectTemplates 2.2.1
dotnet new install FSharp.Data.GraphQL.ProjectTemplates::2.2.1
FSharp.Data.GraphQL
F# 实现 Facebook GraphQL 查询语言规范。
安装项目模板
输入以下命令以安装用于创建Web应用的模板
从GitHub:dotnet new -i "FSharp.Data.GraphQL::2.0.0-ci-*" --nuget-source "https://nuget.pkg.github.com/fsprojects/index.json"
从NuGet:dotnet new -i "FSharp.Data.GraphQL::2.0.0"
快速入门
open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Types
type Person =
{ FirstName: string
LastName: string }
// Define GraphQL type
let PersonType = Define.Object(
name = "Person",
fields = [
// Property resolver will be auto-generated
Define.AutoField("firstName", StringType)
// Asynchronous explicit member resolver
Define.AsyncField("lastName", StringType, resolve = fun context person -> async { return person.LastName })
])
// Include person as a root query of a schema
let schema = Schema(query = PersonType)
// Create an Exector for the schema
let executor = Executor(schema)
// Retrieve person data
let johnSnow = { FirstName = "John"; LastName = "Snow" }
let reply = executor.AsyncExecute(Parser.parse "{ firstName, lastName }", johnSnow) |> Async.RunSynchronously
// #> { data: { "firstName", "John", "lastName", "Snow" } }
它是类型安全的。诸如无效字段或无效返回类型等问题将在编译时进行检查。
ASP.NET / Giraffe / WebSocket(用于GraphQL订阅)的使用
演示
GraphiQL 客户端
转到GraphiQL 示例目录。为了运行它,以调试设置构建并运行Star Wars API 示例项目。这将创建一个兼容GraphQL规范的Giraffe服务器,在端口8086上运行。然后,您需要运行node.js graphiql前端。为此,运行npm i
以获取所有依赖项,然后运行npm run serve | npm run dev
- 这将启动一个运行在http://localhost:8090/上的webpack服务器。然后访问此链接,GraphiQL编辑器应该会出现。您可以通过以下查询进行尝试
{
hero(id:"1000") {
id,
name,
appearsIn,
homePlanet,
friends {
... on Human {
name
}
... on Droid {
name
}
}
}
}
Relay.js 启动包
另一个示例是流行的Relay Starter Kit的F#版本 - 一个使用React.js + Relay的示例应用程序,它使用兼容Relay的服务器API。
要运行它,以调试设置构建FSharp.Data.GraphQL和FSharp.Data.GraphQL.Relay项目。然后通过在FSI中运行server.fsx
脚本来启动服务器。这将启动一个在端口8083上的兼容Relay的F#服务器。然后通过获取所有依赖项(npm i
)并运行它(npm run serve | npm run dev
)来构建node.js前端 - 这将启动使用Relay管理应用程序状态的React应用程序的webpack服务器。您可以在http://localhost:8083/上访问它。
要更新客户端模式,请访问http://localhost:8083/并将响应(即当前F#服务器的查询分析结果)复制并粘贴到data/schema.json中。
流功能
现在,stream
指令具有一些额外功能,例如通过间隔和/或批量大小进行批处理(缓冲)。为了使其工作,必须在SchemaConfig.Directives
列表中放置一个自定义的流指令,该指令包含两个可选参数,分别称为interval
和preferredBatchSize
let customStreamDirective =
let args = [|
Define.Input(
"interval",
Nullable IntType,
defaultValue = Some 2000,
description = "An optional argument used to buffer stream results. ")
Define.Input(
"preferredBatchSize",
Nullable IntType,
defaultValue = None,
description = "An optional argument used to buffer stream results. ") |]
{ StreamDirective with Args = args }
let schemaConfig =
{ SchemaConfig.Default with
Directives = [
IncludeDirective
SkipDirective
DeferDirective
customStreamDirective
LiveDirective ] }
此样板代码可以通过内置实现轻松缩减
let streamOptions =
{ Interval = Some 2000; PreferredBatchSize = None }
let schemaConfig =
SchemaConfig.DefaultWithBufferedStream(streamOptions)
实时查询
现在服务器组件支持 live
指令。为了支持实时查询,需要将模式中每种类型的每个字段配置为实时字段。这是通过使用 ILiveFieldSubscription
和 ILiveQuerySubscriptionProvider
来实现的,它们可以在 SchemaConfig
中进行配置。
type ILiveFieldSubscription =
interface
abstract member Identity : obj -> obj
abstract member TypeName : string
abstract member FieldName : string
end
and ILiveFieldSubscription<'Object, 'Identity> =
interface
inherit ILiveFieldSubscription
abstract member Identity : 'Object -> 'Identity
end
and ILiveFieldSubscriptionProvider =
interface
abstract member HasSubscribers : string -> string -> bool
abstract member IsRegistered : string -> string -> bool
abstract member AsyncRegister : ILiveFieldSubscription -> Async<unit>
abstract member TryFind : string -> string -> ILiveFieldSubscription option
abstract member Add : obj -> string -> string -> IObservable<obj>
abstract member AsyncPublish<'T> : string -> string -> 'T -> Async<unit>
end
要将字段设置为实时字段,调用 Register
扩展方法。每个订阅都需要知道对象标识符,因此必须在 ILiveFieldSubscription
的身份函数中配置。此外,还需要传递 ObjectDef
内部的类型名称和字段名称。
let schemaConfig = SchemaConfig.Default
let schema = Schema(root, config = schemaConfig)
let subscription =
{ Identity = fun (x : Human) -> x.Id
TypeName = "Hero"
FieldName = "name" }
schemaConfig.LiveFieldSubscriptionProvider.Register subscription
这样,英雄字段的名称现在可以通过 live
指令进行实时更新,每次查询时都会同步更新给客户端。要向订阅者发送更新,只需调用 Publish 方法,传递类型名称、字段名称和更新后的对象。
let updatedHero = { hero with Name = "Han Solo - Test" }
schemaConfig.LiveFieldSubscriptionProvider.Publish "Hero" "name" updatedHero
客户端供应商
我们的客户端库现在有一个全新设计的类型供应商。开始使用它之前,首先需要访问您试图连接的服务器的访问检查模式。这可以通过供应商以两种方式之一完成:
- 提供所希望使用的 GraphQL 服务器的 URL(无需任何自定义 HTTP 标头)。供应商将访问服务器,发送一个 Introspection 查询,并使用该模式提供查询中使用的类型。
type MyProvider = GraphQLProvider<"http://some.graphqlserver.development.org">
- 提供供供应商使用的内部检查 JSON 文件。但是请注意,内部检查 JSON 应包含供应商所需的全部字段。您可以通过在目标服务器上运行 我们的标准内部检查查询 并将其保存到与使用供应商的项目相同的路径上的文件来获取正确的字段。
type MyProvider = GraphQLProvider<"swapi_schema.json">
从此以后,您就可以开始执行查询和突变。
let operation =
MyProvider.Operation<"""query q {
hero (id: "1001") {
name
appearsIn
homePlanet
friends {
... on Human {
name
homePlanet
}
... on Droid {
name
primaryFunction
}
}
}
}""">()
// This is a instance of GraphQLProviderRuntimeContext.
// You can use it to provider a runtime URL to access your server,
// and optionally additional HTTP headers (auth headers, for example).
// If you use a local introspection file to parse the schema,
// The runtime context is mandatory.
let runtimeContext =
{ ServerUrl = "http://some.graphqlserver.production.org"
CustomHttpHeaders = None }
let result = operation.Run(runtimeContext)
// Query result objects have pretty-printing and structural equality.
printfn "Data: %A\n" result.Data
printfn "Errors: %A\n" result.Errors
printfn "Custom data: %A\n" result.CustomData
// Response from the server:
// Data: Some
// {Hero = Some
// {Name = Some "Darth Vader";
// AppearsIn = [|NewHope; Empire; Jedi|];
// HomePlanet = Some "Tatooine";
// Friends = [|Some {Name = Some "Wilhuff Tarkin";
// HomePlanet = <null>;}|];};}
// Errors: <null>
// Custom data: map [("documentId", 1221427401)]
有关如何使用客户端供应商的更多信息,请参阅 示例文件夹。
中间件
您可以在 Executor<'Root>
对象之上创建和使用中间件。
通过 Executor 使用查询执行过程涉及三个阶段
模式编译阶段:此阶段发生在
Executor<'Root>
类被实例化时。在此阶段,使用类型模式映射构建字段执行映射,它包含所有字段定义及其字段解析函数。该映射在之后的计划和执行阶段用于检索查询的字段值。操作计划阶段:此阶段在运行没有执行计划的查询之前发生。此阶段负责分析查询生成的 AST 文档,并构建执行计划。
操作执行阶段:此阶段是执行查询的阶段。它需要一个执行计划,因此它通常在操作计划阶段之后发生。
所有阶段都将所需数据进行包装到一个上下文对象中,并通过内部函数传递
let internal compileSchema (ctx : SchemaCompileContext) : unit =
// ...
let internal planOperation (ctx: PlanningContext) : ExecutionPlan =
// ...
let internal executeOperation (ctx : ExecutionContext) : AsyncVal<GQLResponse> =
// ...
因此,在编译模式阶段,在 SchemaCompileContext
对象内修改模式并进行执行映射生成。在操作计划阶段,使用 PlanningContext
对象的值生成执行计划,最后,此计划与其他值一起通过 ExecutionContext
对象传递给操作执行阶段,该阶段最终使用它们来执行查询并生成一个 GQLResponse
。
因此,可以使用中间件拦截每个阶段并根据需要进行自定义。每个中间件必须实现为具有特定签名的函数,并包裹在 IExecutorMiddleware
接口内
type SchemaCompileMiddleware =
SchemaCompileContext -> (SchemaCompileContext -> unit) -> unit
type OperationPlanningMiddleware =
PlanningContext -> (PlanningContext -> ExecutionPlan) -> ExecutionPlan
type OperationExecutionMiddleware =
ExecutionContext -> (ExecutionContext -> AsyncVal<GQLResponse>) -> AsyncVal<GQLResponse>
type IExecutorMiddleware =
abstract CompileSchema : SchemaCompileMiddleware option
abstract PlanOperation : OperationPlanningMiddleware option
abstract ExecuteOperationAsync : OperationExecutionMiddleware option
可选地,为了便于实现,可以使用从其派生的具体类,在该构造函数中仅接收可选的子中间件函数。
type ExecutorMiddleware(?compile, ?plan, ?execute) =
interface IExecutorMiddleware with
member _.CompileSchema = compile
member _.PlanOperation = plan
member _.ExecuteOperationAsync = execute
每个中间件函数都像一个拦截函数,包含两个参数:阶段的上下文,下一个中间件(或实际阶段本身,通常是最后一个运行的阶段),以及返回值。这些函数可以作为 Executor<'Root>
对象构造函数的参数传递
let middleware = [ ExecutorMiddleware(compileFn, planningFn, executionFn) ]
let executor = Executor(schema, middleware)
一个实际可用的中间件例子可以是测量查询规划所需时间的中间件。这些结果会作为规划上下文中的 Metadata
的一部分返回。 Metadata
对象是 Map<string, obj>
实现的,作为一个信息袋,在各个阶段传递,直到它被包含在 GQLResponse
对象内部。您可以利用它将自定义信息通过中间件传递
let planningMiddleware (ctx : PlanningContext) (next : PlanningContext -> ExecutionPlan) =
let watch = Stopwatch()
watch.Start()
let result = next ctx
watch.Stop()
let metadata = result.Metadata.Add("planningTime", watch.ElapsedMilliseconds)
{ result with Metadata = metadata }
内置中间件
在 FSharp.Data.GraphQL.Server.Middleware
包中包含了一些内置中间件
QueryWeightMiddleware
此中间件可用以给模式中的字段添加权重。这些加权字段现在可以用来防止可能被用于 DDoS 攻击的复杂查询。
定义字段时,我们使用扩展方法 WithQueryWeight
来为其添加权重
let resolveFn (h : Human) =
h.Friends |> List.map getCharacter |> List.toSeq
let field =
Define.Field("friends", ListOf (Nullable CharacterType),
resolve = resolveFn).WithQueryWeight(0.5)
然后我们为 Executor 定义一个阈值中间件。如果我们执行一个递归查询,请求“朋友的朋友”,执行器仅在查询超过权重阈值 2.0 之前接受嵌套 4 次
let middleware = [ Define.QueryWeightMiddleware(2.0) ]
ObjectListFilterMiddleware
此中间件可用于自动生成模式中对象内列表字段的过滤器。此过滤器可以作为查询中字段的参数传递,并在解析函数的 ResolveFieldContext
参数中恢复。
例如,我们可以创建一个中间件,用于过滤 Human
对象的列表字段,这些字段类型为 Character option
let middleware = [ Define.ObjectListFilterMiddleware<Human, Character option>() ]
过滤参数是一个对象,通过字段上的 filter
参数中的 JSON 定义进行映射。一个简单的例子是过滤以字母 A 开头的英雄的朋友
query TestQuery {
hero(id:"1000") {
id
name
appearsIn
homePlanet
friends (filter : { name_starts_with: "A" }) {
id
name
}
}
}
此过滤器通过中间件在 ObjectListFilter
定义中进行映射
type FieldFilter<'Val> =
{ FieldName : string
Value : 'Val }
type ObjectListFilter =
| And of ObjectListFilter * ObjectListFilter
| Or of ObjectListFilter * ObjectListFilter
| Not of ObjectListFilter
| Equals of FieldFilter<System.IComparable>
| GreaterThan of FieldFilter<System.IComparable>
| LessThan of FieldFilter<System.IComparable>
| StartsWith of FieldFilter<string>
| EndsWith of FieldFilter<string>
| Contains of FieldFilter<string>
| FilterField of FieldFilter<ObjectListFilter>
查询中通过过滤器恢复的值在字段的解析函数的 ResolveFieldContext
中可以使用。为了容易访问它,可以使用扩展方法 Filter
,它返回一个 ObjectListFilter option
(如果对象没有实现中间件泛型定义中的列表,或者用户没有提供过滤器输入,则没有值)。
Define.Field("friends", ListOf (Nullable CharacterType),
resolve = fun ctx (d : Droid) ->
ctx.Filter |> printfn "Droid friends filter: %A"
d.Friends |> List.map getCharacter |> List.toSeq)
通过从字段解析上下文中检索此过滤器,可以使用客户端代码自定义对数据库运行的查询,从而扩展您的 GraphQL API 功能。
LiveQueryMiddleware
此中间件可以用于快速允许您的模式字段能够被带有 live
指令的查询调用,假设它们都有一个可以被函数 IdentityNameResolver
发现的唯一属性名。
/// A function that resolves an identity name for a schema object, based on a object definition of it.
type IdentityNameResolver = ObjectDef -> string
例如,如果我们的所有模式对象都有一个名为 Id
的唯一字段,我们就可以像这样使用我们的中间件
let schema = Schema(query = queryType)
let middleware = [ Define.LiveQueryMiddleware(fun _ -> "Id") ]
let executor = Executor(schema, middleware)
不过, IdentityNameResolver
是可选的。如果没有提供解析器函数,则使用此默认实现。另外,正如上面所述,通知订阅者必须通过 Publish
Of ILiveFieldSubscriptionProvider
来进行。
使用扩展来构建您自己的中间件
您可以使用 FSharp.Data.GraphQL.Shared
包提供的扩展方法来帮助构建您自己的中间件。在创建中间件时,通常需要修改模式定义,以向用户代码定义的模式添加功能。例如,ObjectListFilter
中间件就需要修改实现特定类型列表的所有字段,通过接受一个名为 filter
的参数。
默认情况下,字段定义是不可变的,因此有时要生成具有改进特征的副本会比较费力。这就是扩展方法能帮到的地方:例如,如果您需要在模式编译阶段已经定义的字段中添加一个参数,您可以使用FieldDef<'Val>
接口的WithArgs
方法。
let field : FieldDef<'Val> = // Search for field inside ISchema
let arg : Define.Input("id", StringType)
let fieldWithArg = field.WithArgs([ arg ])
要查看用于增强定义的完整扩展列表,您可以查看包含在FSharp.Data.GraphQL.Shared
软件包中的TypeSystemExtensions
模块。
-
net6.0
- FSharp.Core (>= 7.0.403)
NuGet软件包
此软件包没有被任何NuGet软件包使用。
GitHub存储库
此软件包没有被任何流行的GitHub存储库使用。
版本 | 下载 | 最后更新 |
---|---|---|
2.2.1 | 97 | 6/16/2024 |
2.2.0 | 531 | 5/8/2024 |
2.1.0 | 87 | 4/21/2024 |
2.0.0 | 180 | 3/24/2024 |
2.0.0-beta1 | 925 | 2/16/2024 |