스키마 튜토리얼
스키마 튜토리얼레슨 30: 업스트림에서 여러 다운스트림 사이트로 콘텐츠 배포하기

레슨 30: 업스트림에서 여러 다운스트림 사이트로 콘텐츠 배포하기

미디어 회사가 지역별로 WordPress 사이트 네트워크를 운영하고 있으며, 각 뉴스 기사는 해당 지역에 적합한 경우에만 해당 사이트에 게시된다고 가정해 봅시다.

이러한 상황에서는 다음과 같은 아키텍처를 구현하는 것이 합리적입니다.

  • 모든 콘텐츠를 단일 업스트림 WordPress 사이트에 게시(및 편집)하여 콘텐츠의 유일한 신뢰 출처로 기능하게 함
  • 적절한 콘텐츠를 각 지역 다운스트림 WordPress 사이트에 배포함(단, 편집은 하지 않음)

이 튜토리얼 레슨에서는 이 아키텍처의 구현 방법을 설명합니다. 업스트림 WordPress 사이트에는 관련 Gato GraphQL 확장 기능이 활성화되어 있어야 하며, 다운스트림 사이트에는 무료 Gato GraphQL 플러그인만 있으면 됩니다.

업스트림에서 다운스트림 사이트로 콘텐츠를 동기화하는 GraphQL 쿼리

(다운스트림 사이트에만 해당) 이 GraphQL 쿼리가 작동하려면, 엔드포인트에 적용된 스키마 구성에서 중첩 뮤테이션이 활성화되어 있어야 합니다.

아래 GraphQL 쿼리는 업스트림 WordPress 사이트에서 실행되어, 업데이트된 게시물의 콘텐츠를 관련 다운스트림 사이트에 동기화합니다. 사이트 간 공통 식별자로 게시물 슬러그를 사용합니다.

(이 쿼리는 이전 튜토리얼 레슨에서 설명한 것처럼 태그, 카테고리, 작성자, 대표 이미지 등 다른 속성도 동기화하도록 응용할 수 있습니다.)

이 쿼리에는 트랜잭션 로직이 포함되어 있으므로, HTTP 요청이 실패하거나(서버가 다운된 경우 등)GraphQL 쿼리가 오류를 발생시키는 경우(제공된 슬러그를 가진 게시물이 없는 경우 등), 어느 다운스트림 사이트에서든 업데이트가 실패하면 모든 다운스트림 사이트에서 뮤테이션이 되돌려집니다.

상태를 되돌리려면 변수 $previousPostContent를 제공해야 합니다. WordPress의 post_updated 액션에 훅을 걸어 이 값을 전달할 수 있으며, 해당 액션이 발생할 때 GraphQL 쿼리가 실행됩니다(이전 튜토리얼 레슨에서 설명한 대로).

이 쿼리는 다음을 수행합니다.

  • 업데이트된 게시물의 슬러그와 새 콘텐츠 및 이전 콘텐츠를 받음
  • 게시물에서 메타 속성 "downstream_domains"를 가져옴. 이 속성에는 게시물을 배포할 다운스트림 사이트의 도메인 배열이 포함됨
  • 메타 속성이 존재하지 않는 경우(즉, 값이 null인 경우)wp_options 테이블에서 옵션 "downstream_domains"를 가져옴. 이 옵션에는 모든 다운스트림 도메인 목록이 포함됨
  • 각 다운스트림 사이트에 사용자를 로그인시키고(단순화를 위해 동일한 $username$userPassword 사용)게시물 콘텐츠를 업데이트하는 뮤테이션을 실행함
  • 어느 다운스트림 사이트에서든 오류가 발생하면 모든 다운스트림 사이트에서 뮤테이션을 되돌림
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_domains"가 빈 배열로 정의된 경우 해당 게시물은 어느 다운스트림 사이트에도 배포되지 않습니다.

이는 함수 필드 _notNull_notEmpty의 차이 때문에 가능합니다(PHP Functions via Schema 확장 기능에서 제공됨).

  • 메타 속성 "downstream_domains"가 정의되지 않은 경우 값은 null이며, _notNull_notEmpty 모두 false로 평가됩니다
  • 메타 속성 "downstream_domains"가 빈 배열로 정의된 경우 값은 []이며, _notEmptyfalse로 평가됩니다