บทเรียนที่ 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