GraphQL
Работа с GraphQL
DC CMS обладает встроенной поддержкой GraphQL, позволяющей выполнять запросы контента в любом проекте без необходимости дополнительного кодирования. Cхема GraphQL генерируется независимо для каждого проекта на основе конфигурации типов контента, установленной с использованием CMS Studio. Эта схема обновляется автоматически при обнаружении любых изменений.
Для настройки проекта с использованием GraphQL:
- Создайте новый проект (если у вас уже создан проект, перейдите к шагу 3).
- Определите модель контента для вашего проекта.
- Получите схему GraphQL для вашего проекта, что можно сделать с помощью предоставленного клиента GraphiQL или любого стороннего клиента.
- Разработайте запросы GraphQL для использования в вашем проекте или внешних приложениях.
Любые изменения контента, внесенные в CMS Studio, немедленно отобразятся в запросах GraphQL.
Когда вносятся изменения в модель контента, такие как добавление нового поля или установка нового типа контента, схема GraphQL перестраивается для внесения этих изменений. Таким образом, в проекте DC CMS, использующем запросы GraphQL, процесс разработки будет следующим:
- Разработчики определяют основную модель контента.
- Разработчики создают основные запросы GraphQL для сайта, чтобы они соответствовали последней схеме.
- Авторы контента создают контент на основе модели.
- Издатели (публицисты) контента проверяют и утверждают к публикации работу авторов.
- Издатели (публицисты) контента публикуют в live-окружении как конфигурацию модели контента, так и обновления контента.
- CMS Deployer автоматизирует перестройку схемы GraphQL во время развертывания.
Кроме того, у вас есть возможность использовать API GraphQL DC CMS из внешнего проекта или приложения. Однако в таких случаях вы должны управлять перезагрузкой схемы с помощью инструментов сторонних разработчиков.
Использование GraphiQL в CMS Studio
GraphiQL - это простой клиент GraphQL, доступный в CMS Studio, который позволяет выполнять запросы GraphQL и перемещаться по документации схемы сайта без необходимости использования дополнительных инструментов. Чтобы получить доступ к GraphiQL, выполните следующие шаги:
- Войдите в CMS Studio.
- Нажмите на название вашего проекта на экране проектов.
- В левой боковой панели перейдите в Инструменты сайта > GraphQL.
Чтобы изучить схему GraphQL, нажмите на Docs справа.
GraphiQL обладает удобной навигацей для быстрого поиска определенного типа или поля.
Чтобы протестировать запросы GraphQL, введите их в текстовый редактор слева. GraphiQL будет предлагать подсказки и проверит запрос на соответствие схеме в режиме реального времени.
Если используется имя хоста сервера GraphQL, отличное от localhost
, убедитесь, что <graphql-server-url />
в вашем файле конфигурации прокси установлен на правильный URL. Дополнительные сведения о настройке файла прокси можно найти в "Конфигурация прокси".
Примеры GraphQL
Вот несколько примеров, как выполнять запросы контента с использованием GraphQL. Эти примеры основаны на встроенном шаблоне “Web Blog”, но принципы применимы к любому сайту DC CMS.
Для каждого типа контента на сайте существует соответствующее поле в корневом запросе. Имя поля соответствует имени типа контента; например, для /page/article
поле будет называться page_article
. Эти поля содержат два подполя: total
для общего числа элементов, найденных запросом, и items
для списка элементов.
GraphQL разрешает использование только буквенно-цифровых символов и подчеркиваний (_
) в именах, поэтому, если имя вашего типа контента или поля содержит дефис (-
), он будет заменен на двойное подчеркивание (__
). Рекомендуется использовать подчеркивания (_
) или нотацию camelCase, если это возможно.
Один из самых простых запросов GraphQL, который вы можете выполнить на сайтах DC CMS, - это получение всех элементов заданного типа контента.
Запрос для элементов /page/article
:
# root query
{
# query for content-type '/page/article'
page_article {
total # total number of items found
items { # list of items found
# content-type fields that will be returned
# (names are based on the content-type configuration)
title
author
date_dt
}
}
}
Кроме того, вы можете запрашивать все страницы, компоненты или элементы контента (как страницы, так и компоненты):
- запрос для всех страниц
# root query
{
# query for all pages
pages {
total # total number of items found
items { # list of items found
# the page fields that will be returned
content__type
localId
createdDate_dt
lastModifiedDate_dt
placeInNav
orderDefault_f
navLabel
}
}
}
- запрос для всех компонентов
# root query
{
# query for all pages
components {
total # total number of items found
items { # list of items found
# the component fields that will be returned
content__type
localId
createdDate_dt
lastModifiedDate_dt
}
}
}
- запрос для всех элементов контента
# root query
{
# query for all pages
contentItems {
total # total number of items found
items { # list of items found
# the content item fields that will be returned
content__type
localId
createdDate_dt
lastModifiedDate_dt
}
}
}
Однако, если запрос возвращает слишком много элементов, результат может быть слишком большим, поэтому можно использовать пагинацию с помощью параметров offset
и limit
. Например, следующий запрос вернет только первые пять найденных элементов.
Разбитый на страницы запрос для типа контента /page/article
:
# root query
{
# query for content-type '/page/article'
page_article(offset: 0, limit: 5) {
total # total number of items found
items { # list of items found
# content-type fields that will be returned
# (names are based on the content-type configuration)
title
author
date_dt
}
}
}
По умолчанию элементы сортируются по полю lastModifiedDate_dt
в порядке убывания, но вы можете настроить сортировку с помощью параметров sortBy
и sortOrder
. Например, вы можете отсортировать по полю date_dt
, специфичному для типа контента /page/article
.
Разбитый на страницы и отсортированный запрос по типу контента /page/article
:
# root query
{
# query for content-type '/page/article'
page_article (offset: 0, limit: 5, sortBy: "date_dt", sortOrder: ASC) {
total # total number of items found
items { # list of items found
# content-type fields that will be returned
# (names are based on the content-type configuration)
title
author
date_dt
}
}
}
Помимо получения всех элементов для конкретного типа контента, вы можете фильтровать результаты на основе одного или нескольких полей в запросе. Доступны различные фильтры в зависимости от типа поля; например, вы можете фильтровать элементы по определенному автору.
Разбитый на страницы, отсортированный и отфильтрованный запрос по типу контента /page/article
:
# root query
{
# query for content-type '/page/article'
page_article (offset: 0, limit: 5, sortBy: "date_dt", sortOrder: ASC) {
total # total number of items found
items { # list of items found
# content-type fields that will be returned
# (names are based on the content-type configuration)
title
# only return articles from this author
author (filter: { matches: "Jane" })
date_dt
}
}
}
Сложные фильтры можно создавать с использованием выражений типа and
, or
и not
для любого поля.
Отфильтрованный запрос со сложными условиями:
# Root query
{
page_article {
total
items {
title
author
date_dt
# Filter articles that are not featured
featured_b (
filter: {
not: [
{
equals: true
}
]
}
)
# Filter articles from category style or health
categories {
item {
key (
filter: {
or: [
{
matches: "style"
},
{
matches: "health"
}
]
}
)
value_smv
}
}
}
}
}
Кроме того, вы можете включать поля из дочерних компонентов в свою модель, такие как node-selector
, checkbox-group
и repeat groups
. Фильтры также могут быть применены к полям из дочерних компонентов.
Разбитый на страницы, отсортированный и отфильтрованный запрос для типа контента /page/article
с использованием дочерних компонентов:
# root query
{
# query for content-type '/page/article'
page_article (offset: 0, limit: 5, sortBy: "date_dt", sortOrder: ASC) {
total # total number of items found
items { # list of items found
# content-type fields that will be returned
# (names are based on the content-type configuration)
title
# only return articles from this author
author (filter: { matches: "Jane" })
date_dt
categories {
item {
# only return articles from this category
key (filter: { matches: "health" })
value_smv
}
}
}
}
}
Псевдонимы (aliases) GraphQL поддерживаются на корневых уровнях запросов (contentItems
, pages
, components
и полях типов контента).
Запрос статей за 2022 и 2023 годы с использованием псевдонимов (aliases):
# root query
{
# query for 2022 articles
articlesOf2022: page_article {
items {
localId(filter: {regex: ".*2022.*"})
}
},
# query for 2023 articles
articlesOf2023: page_article {
items {
localId(filter: {regex: ".*2023.*"})
}
}
}
Фрагменты GraphQL полностью поддерживаются и могут использоваться встроенно или в виде расширений. Фрагменты позволяют упростить запросы, извлекая повторяющиеся поля или запрашивая конкретные поля для разных типов контента в одном запросе.
Использование расширений фрагментов для упрощения запроса:
# Fragment definition
fragment CommonFields on ContentItem {
localId
createdDate_dt
}
# Root query
query {
page_article {
total
items {
# Fragment spread
... CommonFields
title
author
}
}
component_feature {
total
items {
# Fragment spread
... CommonFields
title
icon
}
}
}
Использование встроенных фрагментов для запроса определенных полей в одном запросе:
# Root query
{
contentItems {
total
items {
# Query for fields from the interface
localId
createdDate_dt
# Query for fields from specific types
... on page_article {
title
author
}
... on component_feature {
title
icon
}
}
}
}
Более подробную информацию о GraphQL вы можете найти в официальной документации.
Кастомизированная схема GraphQL
DC CMS предлагает простой подход к кастомизации встроенной схемы GraphQL. Эта функциональность используется для интеграции внешних сервисов или адаптации значений для удовлетворения конкретных потребностей. После кастомизации схемы вы можете создавать приложения или веб-сайты, которые взаимодействуют, используя только GraphQL для доступа как к созданному контенту, так и к внешним сервисам.
Примечание: Эта статья предполагает, что вы знакомы с основными понятиями GraphQL, такими как тип, поле, распознаватель (resolver) и fetcher. Дополнительную информацию о GraphQL можно найти здесь.
После того, как CMS Engine генерирует типы, соответствующие типам контента в репозитории сайта, он ищет скрипт Groovy для кастомизации схемы перед ее предоставлением клиентам. По умолчанию этот скрипт находится в /scripts/graphql/init.groovy
.
В этом скрипте доступны большинство глобальных переменных, описанных в статье “Разработка на Groovy”, за исключением тех, которые относятся к области запроса. Кроме того, есть глобальная переменная, специфичная для скрипта:
Название | Описание | Тип |
---|---|---|
schema | Содержит пользовательские типы, поля, fetcher-ы и распознаватели (resolvers), которые будут добавлены в схему GraphQL | SchemaCustomizer |
Все настройки кастомизированной схемы должны быть реализованы программно. Более подробная информация и примеры предоставлены в документации по GraphQL для Java.
Пример
В примере ниже показано, как настроить схему для интеграции сервиса, написанного на Groovy.
В примере используется общедоступный OMDb API, для которого требуется ключ. Чтобы код работал в вашем локальном окружении, вы можете получить бесплатный ключ здесь.
1. Обновите конфигурацию сайта (/config/engine/site-config.xml
), включив в нее необходимую информацию для подключения к OMDb API:
<site>
<omdb>
<baseUrl>http://www.omdbapi.com</baseUrl>
<apiKey>XXXXXXX</apiKey>
</omdb>
</site>
2. Обновите контекст сайта (/config/engine/application-context.xml
), чтобы включить новый сервисный компонент (bean):
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"
xmlns:context="http://www.springframework.org/schema/context">
<!-- Enable placeholders support -->
<context:property-placeholder/>
<!-- Define the service bean -->
<bean id="omdbService" init-method="init"
class="ru.dc.cms.movies.omdb.OmdbService">
<property name="baseUrl" value="${omdb.baseUrl}"/>
<property name="apiKey" value="${omdb.apiKey}"/>
</bean>
</beans>
3. Добавьте сервису класс Groovy (/scripts/classes/org/dccms/movies/omdb/OmdbService.groovy
):
package ru.dc.cms.movies.omdb
// include a third-party library for easily calling the API
@Grab(value='io.github.http-builder-ng:http-builder-ng-core:1.0.4', initClass=false)
import groovyx.net.http.HttpBuilder
class OmdbService {
// the base URL for all API calls
String baseUrl
// the API key needed for the calls
String apiKey
// The http client
HttpBuilder http
// creates an instance of the http client with the configured base URL
def init() {
http = HttpBuilder.configure {
request.uri = baseUrl
}
}
// performs a search call, returns the entries as maps
def search(String title) {
return [
http.get() {
// include the needed parameters
request.uri.query = [ apiKey: apiKey, t: title ]
}
].flatten() // return a list even if the API only returns a single entry
}
}
Сервис не выполняет никакого сопоставления или преобразования значений, возвращаемых API. Его единственная функция - преобразовывать ответ в формате JSON в экземпляры карты Groovy. Следовательно, важно, чтобы схема GraphQL соответствовала именам полей, предоставленным API.
4. Определите используемую схему GraphQL. Сначала вам нужно знать, что вернет API, для создания соответствующей схемы. В любом браузере или REST-клиенте выполните вызов http://www.omdbapi.com/?t=XXXX&apikey=XXXXXXX
. В результате вы получите что-то подобное этому:
- Ответ OMDb API для запроса фильмов:
{
"Title": "Hackers",
"Year": "1995",
"Rated": "PG-13",
"Released": "15 Sep 1995",
"Runtime": "107 min",
"Genre": "Comedy, Crime, Drama, Thriller",
"Director": "Iain Softley",
"Writer": "Rafael Moreu",
"Actors": "Jonny Lee Miller, Angelina Jolie, Jesse Bradford, Matthew Lillard",
"Plot": "Hackers are blamed for making a virus that will capsize five oil tankers.",
"Language": "English, Italian, Japanese, Russian",
"Country": "USA",
"Awards": "N/A",
"Poster": "https://m.media-amazon.com/images/M/MV5BNmExMTkyYjItZTg0YS00NWYzLTkwMjItZWJiOWQ2M2ZkYjE4XkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg",
"Ratings": [
{
"Source": "Internet Movie Database",
"Value": "6.2/10"
},
{
"Source": "Rotten Tomatoes",
"Value": "33%"
},
{
"Source": "Metacritic",
"Value": "46/100"
}
],
"Metascore": "46",
"imdbRating": "6.2",
"imdbVotes": "62,125",
"imdbID": "tt0113243",
"Type": "movie",
"DVD": "24 Apr 2001",
"BoxOffice": "N/A",
"Production": "MGM",
"Website": "N/A",
"Response": "True"
}
- Ответ OMDb API для запроса сериалов:
{
"Title": "Friends",
"Year": "1994–2004",
"Rated": "TV-14",
"Released": "22 Sep 1994",
"Runtime": "22 min",
"Genre": "Comedy, Romance",
"Director": "N/A",
"Writer": "David Crane, Marta Kauffman",
"Actors": "Jennifer Aniston, Courteney Cox, Lisa Kudrow, Matt LeBlanc",
"Plot": "Follows the personal and professional lives of six twenty to thirty-something-year-old friends living in Manhattan.",
"Language": "English, Dutch, Italian, French",
"Country": "USA",
"Awards": "Won 1 Golden Globe. Another 68 wins & 211 nominations.",
"Poster": "https://m.media-amazon.com/images/M/MV5BNDVkYjU0MzctMWRmZi00NTkxLTgwZWEtOWVhYjZlYjllYmU4XkEyXkFqcGdeQXVyNTA4NzY1MzY@._V1_SX300.jpg",
"Ratings": [
{
"Source": "Internet Movie Database",
"Value": "8.9/10"
}
],
"Metascore": "N/A",
"imdbRating": "8.9",
"imdbVotes": "696,324",
"imdbID": "tt0108778",
"Type": "series",
"totalSeasons": "10",
"Response": "True"
}
API также предлагает поддержку отдельных эпизодов, но они не будут демонстрироваться в этом примере. Важно отметить, что не все поля, возвращаемые API, могут быть необходимы в схеме GraphQL. В этом примере мы будем включать только ограниченный набор полей.
5. Определите общий тип записи, который включает в себя все общие поля, присутствующие в фильмах и сериалах:
interface OmdbEntry {
Title: String!
Genre: String!
Plot: String!
Actors: [String!]
}
6. Определите конкретные типы для фильмов и сериалов, которые будут содержать все поля родительского типа, но включать новые:
- тип GraphQL для фильмов
type OmdbMovie implements OmdbEntry {
Title: String!
Genre: String!
Plot: String!
Actors: [String!]
Production: String!
}
- тип GraphQL для сериалов
type OmdbSeries implements OmdbEntry {
Title: String!
Genre: String!
Plot: String!
Actors: [String!]
totalSeasons: Int!
}
7. Вызов службы будет доступен через тип оболочки (wrapper type) GraphQL.
type OmdbService {
search(title: String): [OmdbEntry!]
}
8. Добавьте пользовательские настройки схемы GraphQL для создания схемы, определенной на предыдущем этапе:
package graphql
import static graphql.Scalars.GraphQLInt
import static graphql.Scalars.GraphQLString
import static graphql.schema.GraphQLArgument.newArgument
import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition
import static graphql.schema.GraphQLInterfaceType.newInterface
import static graphql.schema.GraphQLList.list
import static graphql.schema.GraphQLNonNull.nonNull
import static graphql.schema.GraphQLObjectType.newObject
// Define the fields common to all types
def entryFields = [
newFieldDefinition()
.name('Title')
.description('The title of the entry')
.type(nonNull(GraphQLString))
.build(),
newFieldDefinition()
.name('Genre')
.description('The genre of the entry')
.type(nonNull(GraphQLString))
.build(),
newFieldDefinition()
.name('Plot')
.description('The plot of the entry')
.type(nonNull(GraphQLString))
.build(),
newFieldDefinition()
.name('Actors')
.description('The main cast of the entry')
.type(list(nonNull(GraphQLString)))
.build()
]
// Define the parent type
def entryType = newInterface()
.name('OmdbEntry')
.description('The generic entry returned by the API')
.fields(entryFields)
.build()
// Define the type for movies
def movieType = newObject()
.name('OmdbMovie')
.description('The entry returned for movies by the API')
// Use the parent type
.withInterface(entryType)
// GraphQL required to repeat all fields from the interface
.fields(entryFields)
.field(newFieldDefinition()
.name('Production')
.description('The studio of the entry')
.type(nonNull(GraphQLString))
)
.build()
def seriesType = newObject()
.name('OmdbSeries')
.description('The entry returned for series by the API')
// Use the parent type
.withInterface(entryType)
// GraphQL required to repeat all fields from the interface
.fields(entryFields)
.field(newFieldDefinition()
.name('totalSeasons')
.description('The number of seasons of the entry')
.type(nonNull(GraphQLInt))
)
.build()
// Add the resolver for the new types
schema.resolver('OmdbEntry', { env ->
// The API returns the type as a field
switch(env.object.Type) {
case 'movie':
return movieType
case 'series':
return seriesType
}
})
// Add the child types to the schema
// (this is needed because they are not used directly in any field)
schema.additionalTypes(movieType, seriesType)
// Add the new fields to the top level type
schema.field(newFieldDefinition()
.name('omdb') // this field is used to wrap the service calls
.description('All operations related to the OMDb API')
.type(newObject() // inline type definition
.name('OmdbService')
.description('Exposes the OMDb Service')
.field(newFieldDefinition()
.name('search')
.description('Performs a search by title')
// uses the parent type, the resolver will define the concrete type
.type(list(nonNull(entryType)))
.argument(newArgument()
.name('title')
.description("The title to search")
.type(GraphQLString)
)
)
)
)
// Add the fetcher for the search field,
schema.fetcher('OmdbService', 'search', { env ->
// calls the Groovy bean passing the needed parameters
applicationContext.omdbService.search(env.getArgument('title'))
})
// Define a fetcher to split the value returned by the API for the Actors
def actorsFetcher = { env -> env.source.Actors?.split(',')*.trim() }
// Add the fetcher to the concrete types
schema.fetcher('OmdbMovie', 'Actors', actorsFetcher)
schema.fetcher('OmdbSeries', 'Actors', actorsFetcher)
9. Проверьте изменения схемы GraphQL. Появилось новое поле omdb.search
, которое может быть вызвано с различными параметрами; можно запрашивать разные поля в зависимости от типа каждого результата. Например, для фильмов возвращается поле Production
, а для сериалов - totalSeasons
.