บทช่วยสอน Schema
บทช่วยสอน Schemaบทเรียนที่ 30: การกระจายเนื้อหาจากไซต์ต้นทางไปยังไซต์ปลายทางหลายแห่ง

บทเรียนที่ 30: การกระจายเนื้อหาจากไซต์ต้นทางไปยังไซต์ปลายทางหลายแห่ง

สมมติว่าบริษัทสื่อแห่งหนึ่งมีเครือข่ายไซต์ WordPress สำหรับภูมิภาคต่าง ๆ โดยบทความข่าวแต่ละชิ้นจะถูกเผยแพร่บนไซต์หรือไม่นั้นขึ้นอยู่กับว่าเหมาะสมกับภูมิภาคนั้นหรือไม่

สำหรับสถานการณ์นี้ การนำสถาปัตยกรรมต่อไปนี้มาใช้ถือเป็นแนวทางที่สมเหตุสมผล:

  • เนื้อหาทั้งหมดถูกเผยแพร่ (และแก้ไข) บนไซต์ WordPress ต้นทางเพียงแห่งเดียว ซึ่งทำหน้าที่เป็นแหล่งข้อมูลที่ถูกต้องเพียงหนึ่งเดียวสำหรับเนื้อหา
  • เนื้อหาที่เหมาะสมจะถูกกระจาย (แต่ไม่ถูกแก้ไข) ไปยังไซต์ WordPress ปลายทางในแต่ละภูมิภาค

บทเรียนนี้จะสาธิตวิธีการนำสถาปัตยกรรมนี้มาใช้งาน โดยไซต์ WordPress ต้นทางจำเป็นต้องเปิดใช้งานส่วนขยาย Gato GraphQL ที่เกี่ยวข้อง ในขณะที่ไซต์ปลายทางต้องการเพียงปลั๊กอิน Gato GraphQL แบบฟรีเท่านั้น

GraphQL query สำหรับซิงโครไนซ์เนื้อหาจากไซต์ต้นทางไปยังไซต์ปลายทาง

(สำหรับไซต์ปลายทางเท่านั้น) เพื่อให้ GraphQL query นี้ทำงานได้ Schema Configuration ที่ใช้กับ endpoint จำเป็นต้องเปิดใช้งาน Nested Mutations

GraphQL query ด้านล่างนี้ถูกประมวลผลบนไซต์ WordPress ต้นทาง เพื่อซิงโครไนซ์เนื้อหาของโพสต์ที่อัปเดตแล้วไปยังไซต์ปลายทางที่เกี่ยวข้อง โดยใช้ slug ของโพสต์เป็นตัวระบุร่วมระหว่างไซต์ต่าง ๆ

(query นี้สามารถปรับให้ซิงโครไนซ์คุณสมบัติอื่น ๆ ได้ด้วย เช่น แท็ก หมวดหมู่ ผู้เขียน และรูปภาพหน้าปก ตามที่อธิบายไว้ในบทเรียนก่อนหน้า)

query นี้มีตรรกะแบบทรานแซกชัน เพื่อว่าเมื่อใดก็ตามที่การอัปเดตล้มเหลวบนไซต์ปลายทางใด ไม่ว่าจะเป็นเพราะคำขอ HTTP ล้มเหลว (เช่นเมื่อเซิร์ฟเวอร์ล่ม) หรือเพราะ GraphQL query สร้างข้อผิดพลาด (เช่นเมื่อไม่มีโพสต์ที่มี slug ตามที่ระบุ) mutation จะถูกย้อนกลับบนไซต์ปลายทางทั้งหมด

ในการย้อนสถานะกลับ จำเป็นต้องระบุตัวแปร $previousPostContent เราสามารถส่งค่านี้ได้โดยการ hook เข้ากับ WordPress action post_updated ซึ่งเมื่อ action นี้เกิดขึ้น GraphQL query จะถูกประมวลผล (ตามที่อธิบายไว้ในบทเรียนก่อนหน้า)

query นี้ทำสิ่งต่อไปนี้:

  • รับ slug ของโพสต์ที่อัปเดตแล้ว พร้อมทั้งเนื้อหาใหม่และเนื้อหาก่อนหน้าของโพสต์นั้น
  • ดึงค่าเมตา "downstream_domains" จากโพสต์ ซึ่งมีอาร์เรย์ของโดเมนของไซต์ปลายทางที่โพสต์ต้องถูกกระจายไป
  • หากค่าเมตาไม่มีอยู่ (กล่าวคือมีค่าเป็น null) จะดึงออปชัน "downstream_domains" จากตาราง wp_options ซึ่งมีรายการโดเมนปลายทางทั้งหมด
  • ทำการล็อกอินผู้ใช้เข้าสู่ไซต์ปลายทางแต่ละแห่ง (โดยใช้ $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