스키마 튜토리얼
스키마 튜토리얼레슨 29: 다른 WordPress 사이트에서 게시물 가져오기

레슨 29: 다른 WordPress 사이트에서 게시물 가져오기

이 튜토리얼 레슨에서는 다른 WordPress 사이트에서 로컬 WordPress 사이트로 게시물을 가져오는 방식으로 WordPress 사이트를 동기화하는 방법을 설명합니다.

다른 WordPress 사이트에서 게시물을 가져오는 GraphQL 쿼리

아래의 GraphQL 쿼리는 상위 웹사이트의 GraphQL 엔드포인트에 연결하여 특정 게시물의 데이터를 가져오고 로컬에 가져옵니다. Gato GraphQL 플러그인(무료 버전)이 상위 웹사이트에도 설치되어 있어야 합니다.

(상위 사이트에 Gato GraphQL이 설치되어 있지 않은 경우, 쿼리를 수정하여 WP REST API 엔드포인트에 연결하도록 할 수 있습니다.)

게시물에서 참조된 리소스는 모두 로컬에 존재해야 합니다.

  • 작성자
  • 대표 이미지 (있는 경우)
  • 카테고리
  • (태그도 마찬가지이지만, 존재하지 않는 경우 게시물과 함께 생성되므로 문제가 되지 않습니다)

상위 사이트와 로컬 사이트 간의 리소스 공통 식별자로 다음을 사용합니다.

  • 카테고리, 태그, 미디어 아이템에는 슬러그
  • 사용자에게는 사용자 이름

리소스 중 하나라도 로컬 사이트에 존재하지 않으면 GraphQL 쿼리는 오류를 출력하고 가져오기를 중단합니다.

query InitializeDynamicVariables
  @configureWarningsOnExportingDuplicateVariable(enabled: false)
{
  initVariablesWithFalse: _echo(value: false)
    @export(as: "requestProducedErrors")
    @export(as: "responseHasErrors")
    @export(as: "postIsMissing")
    @export(as: "postHasAuthor")
    @export(as: "postHasFeaturedImage")
    @export(as: "postHasCategories")
    @export(as: "postHasTags")
    @remove
 
  initVariablesWithNull: _echo(value: null)
    @export(as: "existingAuthorUsername")
    @export(as: "existingFeaturedImageSlug")
    @export(as: "featuredImageMutationInput")
    @remove
 
  initVariablesWithEmptyArray: _echo(value: [])
    @export(as: "existingCategorySlugs")
    @export(as: "existingTagSlugs")
    @remove
}
 
query CheckIfPostExistsLocally($postSlug: String!)
  @depends(on: "InitializeDynamicVariables")
{
  localPost: post(
    by: { slug: $postSlug }
    status: any
  ) {
    id
  }
 
  postAlreadyExists: _notNull(value: $__localPost)
    @export(as: "postAlreadyExists")
}
 
query FailIfPostAlreadyExistsLocally($postSlug: String!)
  @depends(on: "CheckIfPostExistsLocally")
  @include(if: $postAlreadyExists)
{
  errorMessage: _sprintf(
    string: "Post with slug '%s' already exists locally",
    values: [$postSlug]
  ) @remove
 
  _fail(
    message: $__errorMessage
    data: {
      slug: $postSlug
    }
  ) @remove
 
  createPost: _echo(value: null)
}
 
query ConnectToGraphQLAPI(
  $upstreamServerGraphQLEndpointURL: String!
  $postSlug: String!
)
  @depends(on: "FailIfPostAlreadyExistsLocally")
  @skip(if: $postAlreadyExists)
{
  externalData: _sendGraphQLHTTPRequest(input:{
    endpoint: $upstreamServerGraphQLEndpointURL,
    query: """
    
query GetPost($postSlug: String!) {
  post(by: { slug: $postSlug }) {
    id
    slug
    rawTitle
    rawContent
    rawExcerpt
    author {
      id
      username
    }
    featuredImage {
      id
      slug
    }
    categories {
      id
      slug
    }
    tags {
      id
      slug
    }
  }
}
 
    """,
    variables: [
      {
        name: "postSlug",
        value: $postSlug
      }
    ]
  })
    @export(as: "externalData")
 
  requestProducedErrors: _isNull(value: $__externalData)
    @export(as: "requestProducedErrors")
    @remove
}
 
query ValidateResponse
  @depends(on: "ConnectToGraphQLAPI")
  @skip(if: $postAlreadyExists)
  @skip(if: $requestProducedErrors)
{
  responseHasErrors: _propertyIsSetInJSONObject(
    object: $externalData
    by: {
      key: "errors"
    }
  )
    @export(as: "responseHasErrors")
    @remove
 
  postExists: _propertyIsSetInJSONObject(
    object: $externalData
    by: {
      path: "data.post"
    }
  )
    @remove
    
  postIsMissing: _not(value: $__postExists)
    @export(as: "postIsMissing")
    @remove
}
 
query FailIfResponseHasErrors
  @depends(on: "ValidateResponse")
  @skip(if: $postAlreadyExists)
  @skip(if: $requestProducedErrors)
  @skip(if: $postIsMissing)
  @include(if: $responseHasErrors)
{
  errors: _objectProperty(
    object: $externalData,
    by: {
      key: "errors"
    }
  ) @remove
 
  _fail(
    message: "Executing the GraphQL query against the upstream webserver produced error(s)"
    data: {
      errors: $__errors
    }
  ) @remove
 
  createPost: _echo(value: null)
}
 
query FailIfPostNotExists($postSlug: String!)
  @depends(on: "FailIfResponseHasErrors")
  @skip(if: $requestProducedErrors)
  @include(if: $postIsMissing)
{
  errorMessage: _sprintf(
    string: "There is no post with slug '%s' in the origin",
    values: [$postSlug]
  ) @remove
 
  _fail(
    message: $__errorMessage
    data: {
      slug: $postSlug
    }
  ) @remove
  
  createPost: _echo(value: null)
}
 
query ExportInputs
  @depends(on: "FailIfPostNotExists")
  @skip(if: $postAlreadyExists)
  @skip(if: $requestProducedErrors)
  @skip(if: $responseHasErrors)
  @skip(if: $postIsMissing)
{
  postData: _objectProperty(
    object: $externalData,
    by: { path: "data.post" }
  ) @remove
 
  postTitle: _objectProperty(
    object: $__postData,
    by: { key: "rawTitle" }
  )
    @export(as: "postTitle")
    @remove
 
  postContent: _objectProperty(
    object: $__postData,
    by: { key: "rawContent" }
  )
    @export(as: "postContent")
    @remove
 
  postExcerpt: _objectProperty(
    object: $__postData,
    by: { key: "rawExcerpt" }
  )
    @export(as: "postExcerpt")
    @remove
 
  postAuthorUsername: _objectProperty(
    object: $__postData,
    by: { key: "author" }
  )
    @passOnwards(
      as: "author"
    )
    @applyField(
      name: "_notNull",
      arguments: {
        value: $author
      },
      passOnwardsAs: "hasAuthor"
    )
    @if(condition: $hasAuthor)
      @applyField(
        name: "_objectProperty",
        arguments: {
          object: $author,
          by: { key: "username" }
        },
        setResultInResponse: true
      )
    @export(as: "postAuthorUsername")
    @remove
 
  postHasAuthor: _notNull(
    value: $__postAuthorUsername
  )
    @export(as: "postHasAuthor")
    @remove
 
  postFeaturedImageSlug: _objectProperty(
    object: $__postData,
    by: { key: "featuredImage" }
  )
    @passOnwards(
      as: "featuredImage"
    )
    @applyField(
      name: "_notNull",
      arguments: {
        value: $featuredImage
      },
      passOnwardsAs: "hasFeaturedImage"
    )
    @if(condition: $hasFeaturedImage)
      @applyField(
        name: "_objectProperty",
        arguments: {
          object: $featuredImage,
          by: { key: "slug" }
        },
        setResultInResponse: true
      )
    @export(as: "postFeaturedImageSlug")
    @remove
 
  postHasFeaturedImage: _notNull(
    value: $__postFeaturedImageSlug
  )
    @export(as: "postHasFeaturedImage")
    @remove
 
  postCategorySlugs: _objectProperty(
    object: $__postData,
    by: { key: "categories" }
  )
    @underEachArrayItem(
      passValueOnwardsAs: "category"
    )
      @applyField(
        name: "_objectProperty"
        arguments: {
          object: $category,
          by: {
            key: "slug"
          }
        }
        setResultInResponse: true
      )
    @export(as: "postCategorySlugs")
    @remove
 
  postHasCategories: _notEmpty(
    value: $__postCategorySlugs
  )
    @export(as: "postHasCategories")
    @remove
 
  postTagSlugs: _objectProperty(
    object: $__postData,
    by: { key: "tags" }
  )
    @underEachArrayItem(
      passValueOnwardsAs: "tag"
    )
      @applyField(
        name: "_objectProperty"
        arguments: {
          object: $tag,
          by: {
            key: "slug"
          }
        }
        setResultInResponse: true
      )
    @export(as: "postTagSlugs")
    @remove
 
  postHasTags: _notEmpty(
    value: $__postTagSlugs
  )
    @export(as: "postHasTags")
    @remove
}
 
query ExportExistingResources
  @depends(on: "ExportInputs")
  @skip(if: $postAlreadyExists)
  @skip(if: $requestProducedErrors)
  @skip(if: $responseHasErrors)
  @skip(if: $postIsMissing)
{
  existingAuthorByUsername: user(by: { username: $postAuthorUsername })
    @include(if: $postHasAuthor)
  {
    id
    username @export(as: "existingAuthorUsername")
  }
 
  existingFeaturedImageBySlug: mediaItem(by: { slug: $postFeaturedImageSlug })
    @include(if: $postHasFeaturedImage)
  {
    id
    slug @export(as: "existingFeaturedImageSlug")
  }
 
  existingCategoriesBySlug: postCategories(filter: { slugs: $postCategorySlugs })
    @include(if: $postHasCategories)
  {
    id
    slug @export(as: "existingCategorySlugs", type: LIST)
  }
 
  existingTagsBySlug: postTags(filter: { slugs: $postTagSlugs })
    @include(if: $postHasTags)
  {
    id
    slug @export(as: "existingTagSlugs", type: LIST)
  }
}
 
query ExportMissingResources
  @depends(on: "ExportExistingResources")
  @skip(if: $postAlreadyExists)
  @skip(if: $requestProducedErrors)
  @skip(if: $responseHasErrors)
  @skip(if: $postIsMissing)
{
  isAuthorMissing: _notEquals(
    value1: $postAuthorUsername,
    value2: $existingAuthorUsername
  ) @export(as: "isAuthorMissing")
  
  isFeaturedImageMissing: _notEquals(
    value1: $postFeaturedImageSlug,
    value2: $existingFeaturedImageSlug
  ) @export(as: "isFeaturedImageMissing")
 
  missingCategorySlugs: _arrayDiff(
    arrays: [$postCategorySlugs, $existingCategorySlugs]
  ) @export(as: "missingCategorySlugs")
  areCategoriesMissing: _notEmpty(
    value: $__missingCategorySlugs
  ) @export(as: "areCategoriesMissing")
 
  # missingTagSlugs: _arrayDiff(
  #   arrays: [$postTagSlugs, $existingTagSlugs]
  # ) @export(as: "missingTagSlugs")
  # areTagsMissing: _notEmpty(
  #   value: $__missingTagSlugs
  # ) @export(as: "areTagsMissing")
 
  isAnyResourceMissing: _or(
    values: [
      $__isAuthorMissing,
      $__isFeaturedImageMissing,
      $__areCategoriesMissing,
      # $__areTagsMissing,
    ]
  ) @export(as: "isAnyResourceMissing")
}
 
query FailIfAnyResourceIsMissing
  @depends(on: "ExportMissingResources")
  @skip(if: $postAlreadyExists)
  @skip(if: $requestProducedErrors)
  @skip(if: $postIsMissing)
  @skip(if: $responseHasErrors)
  @include(if: $isAnyResourceMissing)
{
  performingValidations: id
    @if(condition: $isAuthorMissing)
      @fail(
        message: "Author is missing in local site"
        data: {
          missingAuthorByUsername: $postAuthorUsername
        }
        condition: ALWAYS
      )
    @if(condition: $isFeaturedImageMissing)
      @fail(
        message: "Featured image is missing in local site"
        data: {
          missingFeaturedImageBySlug: $postFeaturedImageSlug
        }
        condition: ALWAYS
      )
    @if(condition: $areCategoriesMissing)
      @fail(
        message: "Categories are missing in local site"
        data: {
          missingCategoriesBySlug: $missingCategorySlugs
        }
        condition: ALWAYS
      )
    # @if(condition: $areTagsMissing)
    #   @fail(
    #     message: "Tags are missing in local site"
    #     data: {
    #       missingTagBySlug: $missingTagSlugs
    #     }
    #     condition: ALWAYS
    #   )
  
  createPost: _echo(value: null)
}
 
query ExportMutationInputs
  @depends(on: "FailIfAnyResourceIsMissing")
  @skip(if: $postAlreadyExists)
  @skip(if: $requestProducedErrors)
  @skip(if: $responseHasErrors)
  @skip(if: $postIsMissing)
  @skip(if: $isAnyResourceMissing)
{
  featuredImageMutationInput: _echo(value: {
    slug: $postFeaturedImageSlug
  })
    @include(if: $postHasFeaturedImage)
    @export(as: "featuredImageMutationInput")
    @remove
}
 
mutation ImportPost(
  $postSlug: String!
)
  @depends(on: "ExportMutationInputs")
  @skip(if: $postAlreadyExists)
  @skip(if: $requestProducedErrors)
  @skip(if: $responseHasErrors)
  @skip(if: $postIsMissing)
  @skip(if: $isAnyResourceMissing)
{
  createPost(input: {
    status: draft,
    slug: $postSlug
    title: $postTitle
    contentAs: {
      html: $postContent
    },
    excerpt: $postExcerpt
    authorBy: {
      username: $postAuthorUsername
    },
    featuredImageBy: $featuredImageMutationInput,
    categoriesBy: {
      slugs: $postCategorySlugs
    },
    tagsBy: {
      slugs: $postTagSlugs
    }
  }) {
    status
    errors {
      __typename
      ...on ErrorPayload {
        message
      }
    }
    post {
      id
      date
      status
 
      slug
      title
      content
      excerpt
 
      author {
        id
        username
      }
      featuredImage {
        id
        slug
      }
      categories {
        id
        slug
      }
      tags {
        id
        slug
      }
    }
  }
}

(이전 튜토리얼 레슨에서 살펴봤듯이) Field to Input 기능($__field 구문 사용)을 사용하여 필드의 해결된 값을 인접한 _필드_에 전달합니다.

필드의 해결된 값을 _디렉티브_에 전달해야 할 경우에는 대신 @passOnwards 디렉티브를 사용해야 합니다 (이 역시 Field to Input 익스텐션에서 제공됩니다).

이 쿼리:

{
  posts {
    id
    hasComments
    notHasComments: _not(value: $__hasComments)
  }
}

...다음 쿼리와 동일합니다:

{
  posts {
    id
    hasComments
    notHasComments: hasComments
      @passOnwards(as: "postHasComments")
      @applyField(
        name: "_not"
        arguments: {
          value: $postHasComments
        },
        setResultInResponse: true
      )
  }
}

@passOnwards는 필드 값에 대한 계산을 수행할 때 유용합니다. 예를 들어, 위의 GraphQL 쿼리에서는 user 속성이 null이 아닌지 확인한 후, username 속성을 추출(하여 필드 값을 이것으로 덮어쓰기)하는 데 사용됩니다:

  postAuthorUsername: _objectProperty(
    object: $__postData,
    by: { key: "author" }
  )
    @passOnwards(
      as: "author"
    )
    @applyField(
      name: "_notNull",
      arguments: {
        value: $author
      },
      passOnwardsAs: "hasAuthor"
    )
    @if(condition: $hasAuthor)
      @applyField(
        name: "_objectProperty",
        arguments: {
          object: $author,
          by: { key: "username" }
        },
        setResultInResponse: true
      )
    @export(as: "postAuthorUsername")
    @remove

[WIP] 누락된 리소스 자동 가져오기

Gato GraphQL 스키마는 다음을 위한 뮤테이션으로 업그레이드되어야 합니다.

  • 사용자 생성/업데이트 (#2456)
  • 카테고리 생성/업데이트 (#2457)

이 작업은 현재 진행 중입니다. 이러한 뮤테이션이 지원되면 GraphQL 쿼리에서 누락된 각 리소스를 자동으로 가져올 수 있게 됩니다.