在没有SDL的情况下向GraphQL架构添加指令

2020-09-08 11:16:03

创建GraphQL服务器有两种方法:SDL优先(模式定义语言)方法和代码优先方法,这两种方法各有优缺点。并且有两种类型的指令:通过SDL在模式上声明的模式类型指令(例如@Deposated)和添加到客户端查询中的查询类型指令(例如@Include和@Skip)。

代码优先方法的一个缺点是,因为它没有SDL,所以它不能自然地支持模式类型指令。代码优先服务器可以提供通过代码而不是SDL声明模式类型指令的替代方案。然而,GraphQL-js(它是GraphQL的参考实现)的维护者已经决定不支持通过代码注册指令。

因此,依赖于GraphQL-js的代码优先服务器可能无法提供依赖于这些指令的功能,除非它们找到一些解决办法。例如,Nexus不能与Apollo Federation集成,这需要您在模式上定义@Key指令。

但是其他GraphQL服务器在这方面不需要模拟GraphQL-js,并且可以通过代码提供对声明模式类型指令的支持。毕竟,GraphQL规范本身并不关心如何实现指令。

在本文中,我将描述我通过POP(我编写的第一个用PHP编写的代码GraphQL服务器)为GraphQL实现的策略,通过代码提供对模式类型指令的支持。

@Deposated是架构类型指令,因此必须将其应用于架构。但是,如果我们暂时假装它是一个查询类型的指令,并直接在查询中的某个字段上添加@Deposated,会发生什么呢?

嗯,它也会奏效的!因为指令毕竟只是一些在字段上执行的功能;通过模式声明该功能,或者直接在查询中声明该功能,并不会使该功能表现出任何不同。

问题是,即使它可以工作,它也没有任何意义;您不能强制您的客户在他们的查询中添加@Deepated。这是由服务器端的应用程序决定的功能,而不是客户端的功能。

然而,要注意的是,该功能本身仍然有效。因此,从功能的角度来看,指令是添加到模式中还是添加到查询中并不重要。此外,每个指令最终都会出现在查询中,因为它就是在那里执行的。

因此,如果我们没有SDL,我们仍然可以在运行时将指令嵌入到查询中。

在将GraphQL指令视为中间件的文章中,我描述了指令管道,这是一种使服务器引擎能够解析、验证和执行查询的体系结构。为了使引擎尽可能简单,有关查询解析的每个操作都通过指令在管道内进行。

调用解析器来验证和解析字段,并将其输出合并到响应中,这是通过两个特殊指令完成的:@Valid和@ResolveValueAndMerge。这些指令是一种特殊类型;它们不是由应用程序(在查询或模式上)添加的,而是由引擎本身添加的。这两个指令是隐式的,并且它们总是添加到每个查询的每个字段中。

从这个策略可以看出,在GraphQL服务器上执行查询时,实际上涉及两个查询:

最终由服务器解析的可执行查询是通过对请求的查询应用转换而生成的,其中包括每个字段的@VALIDATE和@ResolveValueAndMerge指令。

{POSTS@VALIDATE@RESOLVEValueAndMerge{url@VALIDATE@RESOLVEValueAndMerge Title@VALIDATE@RESOLVEValueAndMerge@UPERCASE CONTENT@VALIDATE@INCLUDE(if:$addContent)@ResolveValueAndMerge}}。

实际上,我们可以转换查询以添加任何指令,而不仅仅是特殊类型的指令。这样,即使没有SDL,我们仍然可以在运行时在字段中插入@Deposated指令。

另外,将查询分离为请求的和可执行的实例还有其他潜在的用途。例如,它可以为扁平链语法问题提供解决方案,将快捷方式Programs.ShortName解析为字符串数组([";name1";,";name2";,...])。而不是对象数组([{ShortName:";name1";},{ShortName:";name2";},...])。通过以下步骤:

将请求查询中的字段Programs.ShortName转换为可执行查询中对应的连接程序{ShortName}。

通过某个@Flatten指令将结果从对象数组转换为预期的字符串数组。

将此结果放在名为Programs.ShortName的条目下的查询对象的响应上。

接下来,我们必须生成一种机制来告诉服务器何时以及如何将指令添加到查询中。我开发的机制基于IFTTT(如果这样,那么那样)的概念,我通过指令将其称为IFTTT。

通常,IFTTT是每当指定事件发生时触发操作的规则。在我们的情况下,事件/动作对是:

如果“在查询中找到字段X”,则“将指令Y附加到字段X”

如果“在查询中找到指令Z”,则“在指令Z之前/之后执行指令Y”

我们如何通过IFTTT向模式添加指令?例如,假设我们想要创建一个自定义指令@Authorize(Role:String!)。要验证执行字段myPosts的用户是否具有预期的角色Author,否则将显示错误。

IFTTT规则定义了上述SDL声明的相同意图:每当请求字段myPosts时,对其执行指令@Authorize(Role:";Author";)。

现在,只要在查询中找到字段myPosts,引擎就会自动将@Authorize(Role:';Author';)附加到可执行查询的该字段。

当遇到指令时,也可以触发IFTTT规则,而不仅仅是字段。例如,规则“只要在查询上找到指令@Translate,就对该字段执行指令@cache(时间:3600)”编码如下:

将IFTTT指令添加到查询是一个递归过程:它将触发一个由IFTTT规则处理的新事件,可能会将其他指令附加到查询,依此类推。

例如,规则“只要找到directive@cache,Execute directive@log”就会记录一个关于字段执行的条目,然后触发一个关于这个新添加的指令的新事件。

使用IFTTT方法通过指令执行功能的一个有趣的副作用是,我们现在可以通过配置添加指令,而不是将适合执行预定操作的指令硬编码到模式中,这适合于使API更加灵活。

例如,我们可以方便地从即插即用的第三方调用操作,或者授予我们的用户(不仅仅是我们的开发人员!)。修改服务行为的机会。

例如,对于WordPress的GraphQL by POP-Powered GraphQL API,我为用户构建了一个界面来配置在架构上应用哪些访问控制规则:

引擎正在现场执行一系列指令,但它事先不知道要执行哪些指令。这些规则由用户通过界面定义,因此存储为IFTTT规则。

来自第三方的一些可能通过配置插入到服务中的指令可能是:

在静态Jamstack站点上调用addComment之后,执行WebHook以重新生成站点。

正如我们已经看到的,我们可以通过编码的IFTTT规则在运行时将指令添加到查询中,这样就绕过了SDL。但是,仅靠这一点还不足以构建一个通用的模式。

例如,假设我们的网站是英文的,我们需要将其翻译成法语。然后,我们可以创建规则“每当请求Post.title和Post.content字段时,将指令@Translate(from:";en";,to:";fr";)附加到该字段。”

{帖子{id Title@Translate(From:";EN";,To:";fr";)Content@Translate(From:";EN";,To:";fr";)}}。

到现在为止还好。但是,如果我们希望在不进行任何处理的情况下检索数据(返回英语),会发生什么情况呢?现在我们不能再这样做了,因为Post.title和Post.content字段将始终附加@Translate指令。

解决方案是在服务器端创建字段别名(GraphQL上的字段别名概念在客户机上执行,而不是在服务器上执行)。然后,我们可以创建以下字段别名:

一个字段及其别名在架构中都可用,并且由解析器以完全相同的方式进行解析,因此Post.title和Post.frenchTitle都将被解析为";Hello world!";。但是,我们可以仅在别名字段上定义IFTTT规则,以便只将别名字段转换为法语:

$directiveArgs=[';=&>;EN';,';到';=&39;fr';];$ifttttManager=IFTTTManagerFacade::getInstance();$iftttManager->;addEntriesForFields(';翻译';[[PostTypeResolver::Class,&39;frenchTitle';,$directiveArgs],[PostTypeResolver::Class,&#。

{帖子{id title frenchTitle@Translate(From:";en";,To:";fr";)content frenchContent@Translate(From:";en";,To:";fr";)}}。

这一节是附注,旨在说明服务器端的别名也可以有其他很好的用途。

在GraphQL中的字段版本化一文中,我描述了如何为我们的模式提供基于字段或指令的版本控制(与演变模式形成对比),其中我们传递一个字段(或指令)参数versionConstraint来指示要使用的字段版本。

字段别名可以是一种方便的机制,用于公开架构中所有版本的字段;我们可以将别名“标记”到特定版本的字段,如下所示:

另一个用途是避免命名自定义指令的空间,这是规范推荐的做法:

定义指令时,建议在指令名称前加上前缀,以明确其使用范围,并防止与本文档未来版本可能指定的指令(其名称中不包括_)发生冲突。例如,Facebook的GraphQL服务使用的自定义指令应该命名为@fb_auth,而不是@auth。

这种做法的问题是它使模式难看,其中@fb_auth不如简单的@auth优雅。更糟糕的是,它在避免冲突方面不是100%可靠的,因为公司可能使用相同的名称空间来标识自己。例如,提供指令@fb_auth的库不仅可以由Facebook生成,也可以由Google的Firebase生成。

别名提供的另一种解决方案是仅在实际发生冲突时才生成指令的别名版本。

例如,如果我们使用的是Facebook提供的指令@auth,并且稍后还需要使用Firebase提供的指令@auth,那么我们才会为它们创建别名,比如@fb_auth和@g_fb_auth。

确实,从理论上讲,使用此策略破坏更改的可能性会增加:如果我们调用指令@auth,然后GraphQL规范要求@auth是规范所需的指令(如@Include和@Skip),则仅对指令进行命名空间是不够的;我们还必须将查询更改为使用新的命名空间名称。

但是,这种情况实际发生的可能性有多大呢?考虑到修改规范的原则不变,引入官方指令(除了几个潜在的例外,如@stream、@defer,可能还有@export)的沉默,遇到命名冲突的可能性几乎为零。

使用指令别名,我们的模式在缺省情况下可以是优雅和易读的,并且只有在需要时才会引入命名空间,而不是总是这样。

到现在为止,你可能已经得出结论,我喜欢指令。如果是这样的话,你是对的。在我看来,指令是GraphQL最强大的特性之一,我相信提供对它们的良好访问应该是任何GraphQL服务器的首要任务之一。

在本文中,我描述了代码优先服务器(没有SDL)如何设法提供对模式类型指令的支持。想法很简单:不是在模式上定义它们,而是在运行时通过IFTTT规则将它们附加到查询。这当然不是唯一的方法,但这就是我为我的GraphQL服务器实现它的方式,它工作得很好。

本文是正在进行的关于概念化、设计和实现GraphQL服务器的系列文章的一部分。本系列的前几篇文章是:

虽然GraphQL有一些用于调试请求和响应的功能,但确保GraphQL可靠地为您的生产应用程序提供资源是事情变得更加困难的地方。如果您对确保对后端或第三方服务的网络请求成功感兴趣,请尝试LogRocket。Https://logrocket.com/signup/LogRocket就像网络应用的硬盘录像机,可以记录你网站上发生的一切。您可以聚合和报告有问题的GraphQL请求,而不是猜测问题发生的原因,从而快速了解问题的根本原因。此外,您还可以跟踪Apollo客户端状态并检查GraphQL查询键-值对。

LogRocket检测您的应用程序以记录基线性能计时,如页面加载时间、到第一个字节的时间、慢速网络请求,以及记录Redux、NgRx和Vuex操作/状态。开始免费监控。