Samouczek schematu
Samouczek schematuLekcja 30: Dystrybucja treści z upstream do wielu stron downstream

Lekcja 30: Dystrybucja treści z upstream do wielu stron downstream

Wyobraźmy sobie, że firma medialna posiada sieć stron WordPress dla różnych regionów, gdzie każdy artykuł jest publikowany na danej stronie tylko wtedy, gdy jest odpowiedni dla tego regionu.

W tej sytuacji sensowne jest wdrożenie architektury, w której:

  • Cała treść jest publikowana (i edytowana) na jednej stronie WordPress upstream, która pełni rolę jedynego źródła prawdy dla treści
  • Odpowiednia treść jest dystrybuowana (ale nie edytowana) do każdej z regionalnych stron WordPress downstream

Ta lekcja tutoriala pokaże, jak zaimplementować tę architekturę, w której strona WordPress upstream musi mieć aktywne odpowiednie rozszerzenia Gato GraphQL, podczas gdy strony downstream potrzebują jedynie darmowej wtyczki Gato GraphQL.

Query GraphQL do synchronizacji treści z upstream do stron downstream

(Tylko dla stron downstream) Aby ta query GraphQL działała, Konfiguracja Schema zastosowana do endpointu musi mieć włączone Zagnieżdżone Mutacje

Poniższa query GraphQL jest wykonywana na stronie WordPress upstream, aby zsynchronizować treść zaktualizowanego posta z odpowiednimi stronami downstream, używając sluga posta jako wspólnego identyfikatora między stronami.

(Query można dostosować, aby również synchronizować inne właściwości — tagi, kategorie, autora i wyróżniony obraz — jak wyjaśniono w poprzedniej lekcji tutoriala.)

Query zawiera logikę transakcyjną, tak że gdy aktualizacja nie powiedzie się na którejkolwiek stronie downstream, czy to dlatego, że żądanie HTTP nie powiodło się (np. gdy serwer jest niedostępny), czy dlatego, że query GraphQL zwróciła błędy (np. gdy nie ma posta o podanym slugu), mutacja jest wówczas cofana na wszystkich stronach downstream.

Aby cofnąć stan, należy podać zmienną $previousPostContent. Możemy przekazać tę wartość, podpinając się pod akcję WordPress post_updated, w momencie gdy wykonywana jest query GraphQL (jak wyjaśniono w poprzedniej lekcji tutoriala).

Query wykonuje następujące czynności:

  • Odbiera slug zaktualizowanego posta oraz jego nową i poprzednią treść
  • Pobiera właściwość meta "downstream_domains" z posta, która zawiera tablicę z domenami stron downstream, do których post ma być dystrybuowany
  • Jeśli właściwość meta nie istnieje (tzn. ma wartość null), pobiera opcję "downstream_domains" z tabeli wp_options, która zawiera listę wszystkich domen downstream
  • Loguje użytkownika na każdej ze stron downstream (używając tego samego $username i $userPassword, dla uproszczenia) i wykonuje mutację aktualizującą treść posta
  • Jeśli którakolwiek strona downstream zwróci błąd, mutacja jest cofana na wszystkich stronach 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
}

W powyższej query GraphQL post nie zostanie dystrybuowany do żadnej strony downstream, gdy jego właściwość meta "downstream_domains" jest zdefiniowana z pustą tablicą jako wartość.

Jest to możliwe dzięki różnicy między polami funkcji _notNull i _notEmpty (dostarczanymi przez rozszerzenie PHP Functions via Schema):

  • Jeśli właściwość meta "downstream_domains" nie jest zdefiniowana, jej wartość wynosi null, i zarówno _notNull, jak i _notEmpty zwracają false
  • Jeśli właściwość meta "downstream_domains" jest zdefiniowana jako pusta tablica, jej wartość wynosi [], i tylko _notEmpty zwraca false