Hướng dẫn schema
Hướng dẫn schemaBài 30: Phân phối nội dung từ một upstream đến nhiều site downstream

Bài 30: Phân phối nội dung từ một upstream đến nhiều site downstream

Giả sử một công ty truyền thông có mạng lưới các site WordPress cho nhiều khu vực khác nhau, trong đó mỗi bài viết tin tức chỉ được đăng lên một site nếu nội dung phù hợp với khu vực đó.

Trong trường hợp này, việc triển khai một kiến trúc như sau là hợp lý:

  • Toàn bộ nội dung được đăng (và chỉnh sửa) trên một site WordPress upstream duy nhất, đóng vai trò là nguồn dữ liệu chính xác duy nhất cho nội dung
  • Nội dung phù hợp được phân phối đến (nhưng không chỉnh sửa tại) từng site WordPress downstream theo khu vực

Bài học hướng dẫn này sẽ trình bày cách triển khai kiến trúc này, trong đó site WordPress upstream cần có các tiện ích mở rộng Gato GraphQL liên quan được kích hoạt, còn các site downstream chỉ cần cài đặt plugin Gato GraphQL miễn phí.

Queries GraphQL để đồng bộ hóa nội dung từ upstream đến các site downstream

(Chỉ dành cho các site downstream) Để queries GraphQL này hoạt động, Cấu hình Schema được áp dụng cho endpoint cần bật Nested Mutations

Queries GraphQL dưới đây được thực thi trên site WordPress upstream, để đồng bộ hóa nội dung của bài viết đã được cập nhật đến các site downstream liên quan, sử dụng slug của bài viết làm mã định danh chung giữa các site.

(Queries có thể được điều chỉnh để đồng bộ hóa cả các thuộc tính khác — thẻ, danh mục, tác giả và ảnh đại diện —, như đã giải thích trong bài học hướng dẫn trước.)

Queries bao gồm logic giao dịch, sao cho bất cứ khi nào cập nhật thất bại trên bất kỳ site downstream nào, dù là do yêu cầu HTTP thất bại (như khi máy chủ ngừng hoạt động) hay do queries GraphQL tạo ra lỗi (chẳng hạn như khi không có bài viết nào với slug đã cung cấp), thì mutation sẽ được hoàn tác trên tất cả các site downstream.

Để hoàn tác trạng thái, biến $previousPostContent phải được cung cấp. Chúng ta có thể truyền giá trị này bằng cách móc vào action WordPress post_updated, tại thời điểm đó queries GraphQL được thực thi (như đã giải thích trong bài học hướng dẫn trước).

Queries thực hiện các bước sau:

  • Nhận slug của bài viết đã được cập nhật, cùng với nội dung mới và nội dung trước đó của nó
  • Lấy thuộc tính meta "downstream_domains" từ bài viết, chứa một mảng gồm các domain của các site downstream mà bài viết cần được phân phối đến
  • Nếu thuộc tính meta không tồn tại (tức là có giá trị null), thì lấy tùy chọn "downstream_domains" từ bảng wp_options, chứa danh sách tất cả các domain downstream
  • Đăng nhập người dùng vào từng site downstream (sử dụng cùng $username$userPassword, để đơn giản) và thực thi mutation để cập nhật nội dung bài viết
  • Nếu bất kỳ site downstream nào tạo ra lỗi, mutation sẽ được hoàn tác trên tất cả các site 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
}

Trong queries GraphQL ở trên, một bài viết sẽ không được phân phối đến bất kỳ site downstream nào khi thuộc tính meta "downstream_domains" của nó được định nghĩa với giá trị là mảng rỗng.

Điều này khả thi nhờ sự khác biệt giữa các trường hàm _notNull_notEmpty (được cung cấp bởi tiện ích mở rộng PHP Functions via Schema):

  • Nếu thuộc tính meta "downstream_domains" không được định nghĩa, giá trị của nó là null, và cả _notNull lẫn _notEmpty đều trả về false
  • Nếu thuộc tính meta "downstream_domains" được định nghĩa là mảng rỗng, giá trị của nó là [], và chỉ _notEmpty trả về false