Schema 教程第29课:从另一个 WordPress 站点导入文章
第29课:从另一个 WordPress 站点导入文章
本教程课程演示如何通过将文章从某个 WordPress 站点导入到本地 WordPress 站点,来保持多个 WordPress 站点的同步。
从另一个 WordPress 站点导入文章的 GraphQL Query
以下 GraphQL Query 连接到上游网站的 GraphQL 端点,获取特定文章的数据并将其导入本地。Gato GraphQL 插件(免费版)也必须安装在上游网站上。
(如果上游站点未安装 Gato GraphQL,可以将 Query 改为连接其 WP REST API 端点。)
文章中引用的所有资源必须在本地已存在:
- 作者
- 特色图片(如有)
- 分类
- (标签也是如此,但如果不存在,它们会与文章一起创建,因此不成问题)
上游站点与本地站点之间用于识别资源的通用标识符为:
- 分类、标签和媒体项使用 slug
- 用户使用用户名
如果任何资源在本地站点不存在,GraphQL Query 将输出错误并中止导入。
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 扩展提供)。
以下 Query:
{
posts {
id
hasComments
notHasComments: _not(value: $__hasComments)
}
}...等价于以下 Query:
{
posts {
id
hasComments
notHasComments: hasComments
@passOnwards(as: "postHasComments")
@applyField(
name: "_not"
arguments: {
value: $postHasComments
},
setResultInResponse: true
)
}
}@passOnwards 适用于对字段值进行某些计算的场景。例如,在上面的 GraphQL Query 中,它用于先检查 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 的 Schema 需要通过以下 mutation 进行扩展:
这是一项正在进行中的工作。一旦这些 mutation 得到支持,GraphQL Query 便可自动导入每个缺失的资源。