GraphQL

В этой статье вы узнаете о том, как использовать GraphQL в DC CMS и как кастомизировать встроенную схему GraphQL.

Работа с GraphQL Copy-icon

DC CMS обладает встроенной поддержкой GraphQL, позволяющей выполнять запросы контента в любом проекте без необходимости дополнительного кодирования. Cхема GraphQL генерируется независимо для каждого проекта на основе конфигурации типов контента, установленной с использованием CMS Studio. Эта схема обновляется автоматически при обнаружении любых изменений.

Для настройки проекта с использованием GraphQL:

  1. Создайте новый проект (если у вас уже создан проект, перейдите к шагу 3).
  2. Определите модель контента для вашего проекта.
  3. Получите схему GraphQL для вашего проекта, что можно сделать с помощью предоставленного клиента GraphiQL или любого стороннего клиента.
  4. Разработайте запросы GraphQL для использования в вашем проекте или внешних приложениях.

Любые изменения контента, внесенные в CMS Studio, немедленно отобразятся в запросах GraphQL.

Когда вносятся изменения в модель контента, такие как добавление нового поля или установка нового типа контента, схема GraphQL перестраивается для внесения этих изменений. Таким образом, в проекте DC CMS, использующем запросы GraphQL, процесс разработки будет следующим:

  1. Разработчики определяют основную модель контента.
  2. Разработчики создают основные запросы GraphQL для сайта, чтобы они соответствовали последней схеме.
  3. Авторы контента создают контент на основе модели.
  4. Издатели (публицисты) контента проверяют и утверждают к публикации работу авторов.
  5. Издатели (публицисты) контента публикуют в live-окружении как конфигурацию модели контента, так и обновления контента.
  6. CMS Deployer автоматизирует перестройку схемы GraphQL во время развертывания.

Кроме того, у вас есть возможность использовать API GraphQL DC CMS из внешнего проекта или приложения. Однако в таких случаях вы должны управлять перезагрузкой схемы с помощью инструментов сторонних разработчиков.

Использование GraphiQL в CMS Studio Copy-icon

GraphiQL - это простой клиент GraphQL, доступный в CMS Studio, который позволяет выполнять запросы GraphQL и перемещаться по документации схемы сайта без необходимости использования дополнительных инструментов. Чтобы получить доступ к GraphiQL, выполните следующие шаги:

  1. Войдите в CMS Studio.
  2. Нажмите на название вашего проекта на экране проектов.
  3. В левой боковой панели перейдите в Инструменты сайта > 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
    }
  }
}

Copy-icon

Кроме того, вы можете запрашивать все страницы, компоненты или элементы контента (как страницы, так и компоненты):

  • запрос для всех страниц

# 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
    }
  }
}

Copy-icon
  • запрос для всех компонентов

# 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
    }
  }
}

Copy-icon
  • запрос для всех элементов контента

# 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
    }
  }
}

Copy-icon

Однако, если запрос возвращает слишком много элементов, результат может быть слишком большим, поэтому можно использовать пагинацию с помощью параметров 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
    }
  }
}

Copy-icon

По умолчанию элементы сортируются по полю 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
    }
  }
}

Copy-icon

Помимо получения всех элементов для конкретного типа контента, вы можете фильтровать результаты на основе одного или нескольких полей в запросе. Доступны различные фильтры в зависимости от типа поля; например, вы можете фильтровать элементы по определенному автору.

Разбитый на страницы, отсортированный и отфильтрованный запрос по типу контента /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
    }
  }
}

Copy-icon

Сложные фильтры можно создавать с использованием выражений типа andor и 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
        }
      }
    }
  }
}

Copy-icon

Кроме того, вы можете включать поля из дочерних компонентов в свою модель, такие как node-selectorcheckbox-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
        }
      }
    }
  }
}

Copy-icon

Псевдонимы (aliases) GraphQL поддерживаются на корневых уровнях запросов (contentItemspagescomponents и полях типов контента).

Запрос статей за 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.*"})
    }
  }
}

Copy-icon

Фрагменты 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
    }
  }
}

Copy-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
      }
    }
  }
}

Copy-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>

Copy-icon

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>

Copy-icon

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
  }

}

Copy-icon

Сервис не выполняет никакого сопоставления или преобразования значений, возвращаемых 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"
}

Copy-icon
  • Ответ 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"
}

Copy-icon

API также предлагает поддержку отдельных эпизодов, но они не будут демонстрироваться в этом примере. Важно отметить, что не все поля, возвращаемые API, могут быть необходимы в схеме GraphQL. В этом примере мы будем включать только ограниченный набор полей.

5. Определите общий тип записи, который включает в себя все общие поля, присутствующие в фильмах и сериалах:

interface OmdbEntry {
  Title: String!
  Genre: String!
  Plot: String!
  Actors: [String!]
}

Copy-icon

6. Определите конкретные типы для фильмов и сериалов, которые будут содержать все поля родительского типа, но включать новые:

  • тип GraphQL для фильмов

type OmdbMovie implements OmdbEntry {
  Title: String!
  Genre: String!
  Plot: String!
  Actors: [String!]

  Production: String!
}

Copy-icon
  • тип GraphQL для сериалов

type OmdbSeries implements OmdbEntry {
  Title: String!
  Genre: String!
  Plot: String!
  Actors: [String!]

  totalSeasons: Int!
}

Copy-icon

7. Вызов службы будет доступен через тип оболочки (wrapper type) GraphQL.

type OmdbService {

  search(title: String): [OmdbEntry!]

}

Copy-icon

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)

Copy-icon

9. Проверьте изменения схемы GraphQL. Появилось новое поле omdb.search, которое может быть вызвано с различными параметрами; можно запрашивать разные поля в зависимости от типа каждого результата. Например, для фильмов возвращается поле Production, а для сериалов - totalSeasons.

Связанные статьи

API