Урок 30: Распространение контента с upstream-сайта на несколько downstream-сайтов
Предположим, что медиакомпания имеет сеть WordPress-сайтов для различных регионов, и каждая новостная статья публикуется на том или ином сайте только в том случае, если она подходит для этого региона.
В такой ситуации имеет смысл реализовать архитектуру, при которой:
- Весь контент публикуется (и редактируется) на одном upstream-сайте WordPress, который является единственным источником истины для контента
- Подходящий контент распространяется (но не редактируется) на каждый из региональных downstream-сайтов WordPress
В этом уроке туториала будет показано, как реализовать данную архитектуру: на upstream-сайте WordPress должны быть активированы соответствующие расширения Gato GraphQL, тогда как downstream-сайтам достаточно иметь бесплатный плагин Gato GraphQL.
GraphQL-запрос для синхронизации контента с upstream на downstream-сайты
(Только для downstream-сайтов) Чтобы этот GraphQL-запрос работал, Конфигурация схемы, применяемая к эндпоинту, должна иметь включённые Вложенные мутации
Приведённый ниже GraphQL-запрос выполняется на upstream-сайте WordPress для синхронизации контента обновлённой записи с соответствующими downstream-сайтами, используя слаг записи в качестве общего идентификатора между сайтами.
(Запрос можно адаптировать для синхронизации и других свойств — тегов, категорий, автора и featured-изображения, — как описано в предыдущем уроке туториала.)
Запрос включает транзакционную логику, так что в случае сбоя обновления на любом downstream-сайте — будь то из-за ошибки HTTP-запроса (например, когда сервер недоступен) или из-за ошибок, возвращённых GraphQL-запросом (например, если запись с указанным слагом не найдена), — мутация откатывается на всех downstream-сайтах.
Для отката состояния необходимо передать переменную $previousPostContent. Это значение можно передать, подключившись к WordPress-экшену post_updated, при срабатывании которого выполняется GraphQL-запрос (как описано в одном из предыдущих уроков туториала).
Запрос выполняет следующее:
- Получает слаг обновлённой записи, её новый и предыдущий контент
- Извлекает мета-свойство
"downstream_domains"из записи, которое содержит массив доменов downstream-сайтов, на которые должна быть распространена запись - Если мета-свойство отсутствует (то есть имеет значение
null), извлекает опцию"downstream_domains"из таблицыwp_options, которая содержит список всех downstream-доменов - Выполняет вход пользователя на каждом из downstream-сайтов (используя одинаковые
$usernameи$userPasswordдля простоты) и выполняет мутацию для обновления контента записи - Если какой-либо downstream-сайт возвращает ошибку, мутация откатывается на всех downstream-сайтах
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-запросе запись не будет распространена ни на один downstream-сайт, если её мета-свойство "downstream_domains" определено со значением в виде пустого массива.
Это возможно благодаря разнице между полями-функциями _notNull и _notEmpty (предоставляемыми расширением PHP Functions via Schema):
- Если мета-свойство
"downstream_domains"не определено, его значение равноnull, и оба —_notNullи_notEmpty— возвращаютfalse - Если мета-свойство
"downstream_domains"определено как пустой массив, его значение равно[], и только_notEmptyвозвращаетfalse