Schema 教程第30课:将内容从上游分发到多个下游站点
第30课:将内容从上游分发到多个下游站点
假设一家媒体公司拥有针对不同地区的 WordPress 站点网络,每篇新闻文章只有在适合该地区时才会发布到相应站点。
对于这种情况,实现以下架构是合理的:
- 所有内容发布到(并在)单一上游 WordPress 站点中进行编辑,该站点作为内容的唯一真实来源
- 适当的内容分发到各个地区的下游 WordPress 站点(但不在其中编辑)
本教程课程将演示如何实现此架构:上游 WordPress 站点需要激活相关的 Gato GraphQL 扩展,而下游站点只需安装免费的 Gato GraphQL 插件。
将内容从上游同步到下游站点的 GraphQL Query
(仅限下游站点) 若要使此 GraphQL query 正常工作,应用于端点的架构配置需要启用嵌套 Mutation
以下 GraphQL query 在上游 WordPress 站点上执行,使用文章 slug 作为跨站点的通用标识符,将已更新文章的内容同步到相关的下游站点。
(如前一教程课程所述,该 query 也可以进行调整,以同步其他属性——标签、分类、作者和特色图片。)
该 query 包含事务逻辑,因此当任何下游站点上的更新失败时——无论是 HTTP 请求失败(例如服务器宕机)还是 GraphQL query 产生错误(例如不存在具有所提供 slug 的文章)——所有下游站点上的 mutation 都会被撤销。
要撤销状态,必须提供变量 $previousPostContent。我们可以通过挂钩 WordPress 的 post_updated 动作来传递此值,该动作触发时会执行 GraphQL query(如前一教程课程所述)。
该 query 执行以下操作:
- 接收已更新文章的 slug 及其新内容和旧内容
- 从文章中检索元属性
"downstream_domains",其中包含文章需要分发到的下游站点域名数组 - 如果元属性不存在(即其值为
null),则从wp_options表中检索选项"downstream_domains",其中包含所有下游域名的列表 - 将用户登录到每个下游站点(为简便起见使用相同的
$username和$userPassword)并执行 mutation 以更新文章内容 - 如果任何下游站点产生错误,则在所有下游站点上撤销该 mutation
query InitializeDynamicVariables
@configureWarningsOnExportingDuplicateVariable(enabled: false)
{
initVariablesWithFalse: _echo(value: false)
@export(as: "requestProducedErrors")
@export(as: "anyErrorProduced")
@export(as: "hasDownstreamDomains")
@remove
}
query GetCustomDownstreamDomains($postSlug: String!)
@depends(on: "InitializeDynamicVariables")
{
post(by: { slug: $postSlug }, status: any)
@fail(
message: "There is no post in the upstream site with the provided slug"
data: {
slug: $postSlug
}
)
{
customDownstreamDomains: metaValues(key: "downstream_domains")
@export(as: "downstreamDomains")
hasDefinedCustomDownstreamDomains: _notNull(value: $__customDownstreamDomains)
@export(as: "hasDefinedCustomDownstreamDomains")
@remove
hasCustomDownstreamDomains: _notEmpty(value: $__customDownstreamDomains)
@export(as: "hasDownstreamDomains")
}
isMissingPostInUpstream: _isNull(value: $__post)
@export(as: "isMissingPostInUpstream")
}
query GetAllDownstreamDomains
@depends(on: "GetCustomDownstreamDomains")
@skip(if: $isMissingPostInUpstream)
@skip(if: $hasDefinedCustomDownstreamDomains)
{
allDownstreamDomains: optionValues(name: "downstream_domains")
@export(as: "downstreamDomains")
hasAllDownstreamDomains: _notEmpty(value: $__allDownstreamDomains)
@export(as: "hasDownstreamDomains")
}
############################################################
# (By default) Append "/graphql" to the domain, to point
# to that site's GraphQL single endpoint
############################################################
query ExportDownstreamGraphQLEndpointsAndQuery(
$endpointPath: String! = "/graphql"
)
@depends(on: "GetAllDownstreamDomains")
@skip(if: $isMissingPostInUpstream)
@include(if: $hasDownstreamDomains)
{
downstreamGraphQLEndpoints: _echo(value: $downstreamDomains)
@underEachArrayItem(
passValueOnwardsAs: "domain"
)
@strAppend(string: $endpointPath)
@export(as: "downstreamGraphQLEndpoints")
query: _echo(value: """
mutation LoginUserAndUpdatePost(
$username: String!
$userPassword: String!
$postSlug: String!
$postContent: String!
) {
loginUser(by: {
credentials: {
usernameOrEmail: $username,
password: $userPassword
}
}) {
userID
}
post(by: {slug: $postSlug})
@fail(
message: "There is no post in the downstream site with the provided slug"
data: {
slug: $postSlug
}
)
{
update(input: {
contentAs: { html: $postContent },
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
post {
slug
rawContent
}
}
}
}
"""
)
@export(as: "query")
@remove
}
query ExportSendGraphQLHTTPRequestInputs(
$username: String!
$userPassword: String!
$postSlug: String!
$newPostContent: String!
)
@depends(on: "ExportDownstreamGraphQLEndpointsAndQuery")
@skip(if: $isMissingPostInUpstream)
@include(if: $hasDownstreamDomains)
{
sendGraphQLHTTPRequestInputs: _echo(value: $downstreamGraphQLEndpoints)
@underEachArrayItem(
passValueOnwardsAs: "endpoint"
)
@applyField(
name: "_echo",
arguments: {
value: {
endpoint: $endpoint,
query: $query,
variables: [
{
name: "username",
value: $username
},
{
name: "userPassword",
value: $userPassword
},
{
name: "postSlug",
value: $postSlug
},
{
name: "postContent",
value: $newPostContent
}
]
}
},
setResultInResponse: true
)
@export(as: "sendGraphQLHTTPRequestInputs")
@remove
}
query SendGraphQLHTTPRequests
@depends(on: "ExportSendGraphQLHTTPRequestInputs")
@skip(if: $isMissingPostInUpstream)
@include(if: $hasDownstreamDomains)
{
downstreamGraphQLResponses: _sendGraphQLHTTPRequests(
inputs: $sendGraphQLHTTPRequestInputs
)
@export(as: "downstreamGraphQLResponses")
requestProducedErrors: _isNull(value: $__downstreamGraphQLResponses)
@export(as: "requestProducedErrors")
@export(as: "anyErrorProduced")
@remove
}
query ExportGraphQLResponsesHaveErrors
@depends(on: "SendGraphQLHTTPRequests")
@skip(if: $isMissingPostInUpstream)
@skip(if: $requestProducedErrors)
@include(if: $hasDownstreamDomains)
{
graphQLResponsesHaveErrors: _echo(value: $downstreamGraphQLResponses)
# Check if any GraphQL response has the "errors" entry
@underEachArrayItem(
passValueOnwardsAs: "response"
affectDirectivesUnderPos: [1, 2]
)
@applyField(
name: "_propertyIsSetInJSONObject"
arguments: {
object: $response
by: {
key: "errors"
}
}
setResultInResponse: true
)
@export(as: "graphQLResponsesHaveErrors")
@remove
}
query ValidateGraphQLResponsesHaveErrors
@depends(on: "ExportGraphQLResponsesHaveErrors")
@skip(if: $isMissingPostInUpstream)
@skip(if: $requestProducedErrors)
@include(if: $hasDownstreamDomains)
{
anyGraphQLResponseHasErrors: _or(values: $graphQLResponsesHaveErrors)
@export(as: "anyErrorProduced")
@remove
}
query ExportRevertGraphQLHTTPRequestInputs(
$username: String!
$userPassword: String!
$postSlug: String!
$previousPostContent: String!
)
@depends(on: "ValidateGraphQLResponsesHaveErrors")
@include(if: $hasDownstreamDomains)
@include(if: $anyErrorProduced)
{
revertGraphQLHTTPRequestInputs: _echo(value: $downstreamGraphQLEndpoints)
@underEachArrayItem(
passValueOnwardsAs: "endpoint"
)
@applyField(
name: "_echo",
arguments: {
value: {
endpoint: $endpoint,
query: $query,
variables: [
{
name: "username",
value: $username
},
{
name: "userPassword",
value: $userPassword
},
{
name: "postSlug",
value: $postSlug
},
{
name: "postContent",
value: $previousPostContent
}
]
}
},
setResultInResponse: true
)
@export(as: "revertGraphQLHTTPRequestInputs")
@remove
}
query RevertGraphQLHTTPRequests
@depends(on: "ExportRevertGraphQLHTTPRequestInputs")
@skip(if: $isMissingPostInUpstream)
@include(if: $hasDownstreamDomains)
@include(if: $anyErrorProduced)
{
revertGraphQLResponses: _sendGraphQLHTTPRequests(
inputs: $sendGraphQLHTTPRequestInputs
)
}
query ExecuteAll
@depends(on: "RevertGraphQLHTTPRequests")
{
id @remove
}在上述 GraphQL query 中,当文章的元属性 "downstream_domains" 被定义为空数组时,该文章将不会分发到任何下游站点。
这得益于函数字段 _notNull 和 _notEmpty 之间的差异(由 PHP Functions via Schema 扩展提供):
- 如果元属性
"downstream_domains"未定义,其值为null,_notNull和_notEmpty均评估为false - 如果元属性
"downstream_domains"被定义为空数组,其值为[],只有_notEmpty评估为false