GraphQL — это язык запросов для вашего API и среда выполнения на стороне сервера для выполнения запросов с использованием системы типов, которую вы определяете для своих данных. GraphQL не привязан к какой-либо конкретной базе данных или механизму хранения, а вместо этого поддерживается вашим существующим кодом и данными.
Служба GraphQL создается путем определения типов и полей этих типов, а затем предоставления функций для каждого поля каждого типа. Например, служба GraphQL, которая сообщает вам, кто вошедший в систему пользователь (я), а также имя этого пользователя, может выглядеть так:
type Query {
me: User
}
type User {
id: ID
name: String
}
Наряду с функциями для каждого поля каждого типа:
function Query_me(request) {
return request.auth.user;
}
function User_name(user) {
return user.getName();
}
После запуска службы GraphQL (обычно по URL-адресу веб-службы) она может получать запросы GraphQL для проверки и выполнения. Служба сначала проверяет запрос, чтобы убедиться, что он относится только к определенным типам и полям, а затем запускает предоставленные функции для получения результата.
Например, запрос:
{
me {
name
}
}
Может дать следующий результат JSON:
{
"me": {
"name": "Luke Skywalker"
}
}
Запросы и мутации
На этой странице вы подробно узнаете о том, как запрашивать сервер GraphQL.
Поля
В самом простом случае GraphQL — это запрос определенных полей в объектах. Давайте начнем с рассмотрения очень простого запроса и результата, который мы получим при его выполнении:
{
hero {
name
}
}
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
Сразу видно, что запрос имеет точно такую же форму, как и результат. Это важно для GraphQL, потому что вы всегда получаете то, что ожидаете, и сервер точно знает, какие поля запрашивает клиент.
Имя поля возвращает тип String, в данном случае имя главного героя «Звездных войн» «R2-D2».
О, и еще одно — приведенный выше запрос является интерактивным. Это означает, что вы можете изменить его по своему усмотрению и увидеть новый результат. Попробуйте добавить поле появления в главный объект в запросе и посмотрите на новый результат.
В предыдущем примере мы просто запросили имя нашего героя, которое вернуло строку, но поля также могут относиться к объектам. В этом случае вы можете сделать дополнительный выбор полей для этого объекта. Запросы GraphQL могут проходить по связанным объектам и их полям, позволяя клиентам извлекать большое количество связанных данных за один запрос, вместо того, чтобы выполнять несколько циклических обходов, как это было бы необходимо в классической архитектуре REST.
{
hero {
name
friends {
name
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
Обратите внимание, что в этом примере поле друзей возвращает массив элементов. Запросы GraphQL выглядят одинаково как для отдельных элементов, так и для списков элементов, однако мы знаем, какой из них ожидать, исходя из того, что указано в схеме.
Аргументы
Если бы единственное, что мы могли сделать, — это перемещаться по объектам и их полям, GraphQL уже был бы очень полезным языком для выборки данных. Но когда вы добавляете возможность передавать аргументы полям, все становится намного интереснее.
{
human(id: "1000") {
name
height
}
}
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 1.72
}
}
}
В такой системе, как REST, вы можете передать только один набор аргументов — параметры запроса и сегменты URL в вашем запросе. Но в GraphQL каждое поле и вложенный объект могут получить свой собственный набор аргументов, что делает GraphQL полной заменой множественных выборок API. Вы даже можете передавать аргументы в скалярные поля, чтобы реализовать преобразования данных один раз на сервере, а не на каждом клиенте отдельно.
{
human(id: "1000") {
name
height(unit: FOOT)
}
}
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 5.6430448
}
}
}
Аргументы могут быть самых разных типов. В приведенном выше примере мы использовали тип Enumeration, который представляет один из конечного набора параметров (в данном случае единиц длины, либо METER, либо FOOT). GraphQL поставляется с набором типов по умолчанию, но сервер GraphQL также может объявлять свои собственные пользовательские типы, если они могут быть сериализованы в ваш транспортный формат.
Псевдонимы
Если у вас острый глаз, вы, возможно, заметили, что, поскольку поля объекта результата совпадают с именем поля в запросе, но не включают аргументы, вы не можете напрямую запрашивать одно и то же поле с другими аргументами. Вот почему вам нужны псевдонимы — они позволяют вам переименовывать результат поля во что угодно.
{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
{
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}
В приведенном выше примере два основных поля конфликтуют, но поскольку мы можем присвоить им разные имена, мы можем получить оба результата в одном запросе.
Фрагменты
Допустим, у нас есть относительно сложная страница в нашем приложении, которая позволяет нам смотреть на двух героев рядом с их друзьями. Вы можете себе представить, что такой запрос может быстро усложниться, потому что нам нужно будет повторить поля хотя бы один раз — по одному для каждой стороны сравнения.
Вот почему GraphQL включает повторно используемые единицы, называемые фрагментами. Фрагменты позволяют создавать наборы полей, а затем включать их в запросы там, где это необходимо. Вот пример того, как вы могли бы решить описанную выше ситуацию, используя фрагменты:
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "C-3PO"
},
{
"name": "R2-D2"
}
]
},
"rightComparison": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
Вы можете видеть, что приведенный выше запрос будет довольно повторяющимся, если поля будут повторяться. Концепция фрагментов часто используется для разделения сложных требований к данным приложения на более мелкие фрагменты, особенно когда вам нужно объединить множество компонентов пользовательского интерфейса с разными фрагментами в одну исходную выборку данных.
Использование переменных внутри фрагментов
Фрагменты могут обращаться к переменным, объявленным в запросе или мутации.
query HeroComparison($first: Int = 3) {
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
friendsConnection(first: $first) {
totalCount
edges {
node {
name
}
}
}
}
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"friendsConnection": {
"totalCount": 4,
"edges": [
{
"node": {
"name": "Han Solo"
}
},
{
"node": {
"name": "Leia Organa"
}
},
{
"node": {
"name": "C-3PO"
}
}
]
}
},
"rightComparison": {
"name": "R2-D2",
"friendsConnection": {
"totalCount": 3,
"edges": [
{
"node": {
"name": "Luke Skywalker"
}
},
{
"node": {
"name": "Han Solo"
}
},
{
"node": {
"name": "Leia Organa"
}
}
]
}
}
}
}
Название операции
До сих пор мы использовали сокращенный синтаксис, в котором мы опускали как ключевое слово запроса, так и имя запроса, но в рабочих приложениях полезно использовать их, чтобы сделать наш код менее двусмысленным.
Вот пример, который включает ключевое слово query в качестве типа операции и HeroNameAndFriends в качестве имени операции:
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
Тип операции — запрос, изменение или подписка — описывает тип операции, которую вы собираетесь выполнить. Тип операции требуется, если только вы не используете сокращенный синтаксис запроса, и в этом случае вы не можете указать имя или определения переменных для своей операции.
Имя операции — это осмысленное и явное имя для вашей операции. Это требуется только в документах с несколькими операциями, но его использование рекомендуется, поскольку оно очень полезно для отладки и ведения журнала на стороне сервера. Когда что-то идет не так (вы видите ошибки либо в ваших сетевых журналах, либо в журналах вашего сервера GraphQL), проще идентифицировать запрос в вашей кодовой базе по имени, чем пытаться расшифровать содержимое. Думайте об этом так же, как об имени функции в вашем любимом языке программирования. Например, в JavaScript мы можем легко работать только с анонимными функциями, но когда мы даем функции имя, ее легче отслеживать, отлаживать наш код и регистрировать ее вызов. Точно так же имена запросов и мутаций GraphQL вместе с именами фрагментов могут быть полезным инструментом отладки на стороне сервера для идентификации различных запросов GraphQL.
Переменные
До сих пор мы записывали все наши аргументы внутри строки запроса. Но в большинстве приложений аргументы полей будут динамическими: например, может быть раскрывающийся список, позволяющий выбрать интересующий вас эпизод «Звездных войн», или поле поиска, или набор фильтров.
Было бы не очень хорошей идеей передавать эти динамические аргументы непосредственно в строке запроса, потому что тогда нашему коду на стороне клиента пришлось бы динамически манипулировать строкой запроса во время выполнения и сериализовать ее в формат, специфичный для GraphQL. Вместо этого в GraphQL есть первоклассный способ выделить динамические значения из запроса и передать их как отдельный словарь. Эти значения называются переменными.
Когда мы начинаем работать с переменными, нам нужно сделать три вещи:
Замените статическое значение в запросе на $variableName
Объявите $variableName как одну из переменных, принимаемых запросом.
Передать variableName: значение в отдельном словаре переменных, специфичных для транспорта (обычно JSON).
Вот как это выглядит вместе:
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
{
"episode": "JEDI"
}
Теперь в нашем клиентском коде мы можем просто передать другую переменную, а не создавать совершенно новый запрос. Это также в целом хорошая практика для обозначения того, какие аргументы в нашем запросе должны быть динамическими — мы никогда не должны выполнять интерполяцию строк для построения запросов из введенных пользователем значений.
Определения переменных
Определения переменных — это часть, которая выглядит как ($episode: Episode) в приведенном выше запросе. Он работает точно так же, как определения аргументов для функции в типизированном языке. В нем перечислены все переменные с префиксом $, за которым следует их тип, в данном случае Episode.
Все объявленные переменные должны быть либо скалярами, либо перечислениями, либо типами входных объектов. Поэтому, если вы хотите передать сложный объект в поле, вам нужно знать, какой тип ввода соответствует на сервере. Узнайте больше о типах объектов ввода на странице схемы.
Определения переменных могут быть необязательными или обязательными. В приведенном выше случае, поскольку нет ! рядом с типом эпизода, это необязательно. Но если поле, в которое вы передаете переменную, требует ненулевого аргумента, то переменная также должна быть обязательной.
Чтобы узнать больше о синтаксисе определений этих переменных, полезно изучить язык схемы GraphQL. Язык схемы подробно объясняется на странице схемы.
Переменные по умолчанию
Значения по умолчанию также можно присвоить переменным в запросе, добавив значение по умолчанию после объявления типа.
query HeroNameAndFriends($episode: Episode = JEDI) {
hero(episode: $episode) {
name
friends {
name
}
}
}
Если для всех переменных заданы значения по умолчанию, вы можете вызвать запрос без передачи каких-либо переменных. Если какие-либо переменные передаются как часть словаря переменных, они переопределяют значения по умолчанию.
Директивы
Выше мы обсуждали, как переменные позволяют нам избежать ручной интерполяции строк для построения динамических запросов. Передача переменных в аргументах решает довольно большой класс этих проблем, но нам также может понадобиться способ динамического изменения структуры и формы наших запросов с использованием переменных. Например, мы можем представить компонент пользовательского интерфейса, который имеет сводное и подробное представление, где одно включает больше полей, чем другое.
Построим запрос для такого компонента:
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
{
"episode": "JEDI",
"withFriends": false
}
Попробуйте отредактировать приведенные выше переменные, чтобы они вместо этого передавали значение true для withFriends, и посмотрите, как изменится результат.
Нам нужно было использовать новую функцию в GraphQL, называемую директивой. Директива может быть прикреплена к включению поля или фрагмента и может влиять на выполнение запроса любым способом, который пожелает сервер. Основная спецификация GraphQL включает ровно две директивы, которые должны поддерживаться любой соответствующей спецификации реализацией сервера GraphQL:
@include(if: Boolean) Включить это поле в результат, только если аргумент истинен.
@skip(if: Boolean) Пропустить это поле, если аргумент верен.
Директивы могут быть полезны для выхода из ситуаций, в которых в противном случае вам пришлось бы выполнять манипуляции со строками для добавления и удаления полей в вашем запросе. Реализации сервера также могут добавлять экспериментальные функции, определяя совершенно новые директивы.
Мутации
Большинство дискуссий о GraphQL сосредоточено на выборке данных, но любая полноценная платформа данных также нуждается в способе изменения данных на стороне сервера.
В REST любой запрос может привести к некоторым побочным эффектам на сервере, но по соглашению не рекомендуется использовать запросы GET для изменения данных. GraphQL аналогичен — технически любой запрос может быть реализован для записи данных. Однако полезно установить соглашение о том, что любые операции, вызывающие запись, должны отправляться явно через мутацию.
Как и в запросах, если поле мутации возвращает тип объекта, вы можете запросить вложенные поля. Это может быть полезно для получения нового состояния объекта после обновления. Давайте посмотрим на простой пример мутации:
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
Обратите внимание, как поле createReview возвращает звезды и поля комментариев только что созданного обзора. Это особенно полезно при мутации существующих данных, например, при инкрементировании поля, так как мы можем мутировать и запрашивать новое значение поля одним запросом.
Вы также можете заметить, что в этом примере переменная обзора, которую мы передали, не является скаляром. Это тип входного объекта, особый тип объекта, который может быть передан в качестве аргумента. Узнайте больше о типах ввода на странице схемы.
Несколько полей в мутациях
Мутация может содержать несколько полей, как и запрос. Помимо имени, между запросами и мутациями есть одно важное различие:
В то время как поля запроса выполняются параллельно, поля мутации выполняются последовательно, одно за другим.
Это означает, что если мы отправим две мутации incrementCredits в одном запросе, первая гарантированно завершится до того, как начнется вторая, гарантируя, что мы не попадем в состояние гонки с самими собой.
Встроенные фрагменты
Как и многие другие системы типов, схемы GraphQL включают возможность определять интерфейсы и типы объединения.
Если вы запрашиваете поле, которое возвращает интерфейс или тип объединения, вам нужно будет использовать встроенные фрагменты для доступа к данным базового конкретного типа. Проще всего увидеть на примере:
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
{
"ep": "JEDI"
}
В этом запросе поле героя возвращает тип персонажа, который может быть либо Человеком, либо Дроидом, в зависимости от аргумента эпизода. При прямом выборе вы можете запрашивать только те поля, которые существуют в интерфейсе Персонажа, такие как имя.
Чтобы запросить поле конкретного типа, вам нужно использовать встроенный фрагмент с условием типа. Поскольку первый фрагмент помечен как ... на Droid, поле primaryFunction будет выполняться только в том случае, если персонаж, возвращаемый героем, относится к типу Droid. Аналогично для поля высоты для типа Человек.
Таким же образом можно использовать и именованные фрагменты, поскольку к именованному фрагменту всегда присоединен тип.
Мета поля
Учитывая, что в некоторых ситуациях вы не знаете, какой тип вы получите обратно от службы GraphQL, вам нужен какой-то способ определить, как обрабатывать эти данные на клиенте. GraphQL позволяет запрашивать __typename, метаполе, в любой точке запроса, чтобы получить имя типа объекта в этой точке.
{
search(text: "an") {
__typename
... on Human {
name
}
... on Droid {
name
}
... on Starship {
name
}
}
}
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo"
},
{
"__typename": "Human",
"name": "Leia Organa"
},
{
"__typename": "Starship",
"name": "TIE Advanced x1"
}
]
}
}
В приведенном выше запросе поиск возвращает тип объединения, который может быть одним из трех вариантов. Было бы невозможно отличить разные типы от клиента без поля __typename.
Сервисы GraphQL предоставляют несколько метаполей, остальные используются для предоставления системы Introspection.
Схемы и типы
На этой странице вы узнаете все, что вам нужно знать о системе типов GraphQL и о том, как она описывает, какие данные можно запрашивать. Поскольку GraphQL можно использовать с любым внутренним фреймворком или языком программирования, мы не будем вдаваться в детали реализации и будем говорить только о концепциях.
Система типов
Если вы уже видели запрос GraphQL, вы знаете, что язык запросов GraphQL в основном касается выбора полей в объектах. Так, например, в следующем запросе:
{
hero {
name
appearsIn
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
Начнем со специального «корневого» объекта
Мы выбираем поле героя на этом
Для объекта, возвращаемого героем, мы выбираем имя и появляется в полях
Поскольку форма запроса GraphQL точно соответствует результату, вы можете предсказать, что вернет запрос, не зная так много о сервере. Но полезно иметь точное описание данных, которые мы можем запросить — какие поля мы можем выбрать? Какие объекты они могут вернуть? Какие поля доступны для этих подобъектов? Вот и получается схема.
Каждый сервис GraphQL определяет набор типов, которые полностью описывают набор возможных данных, которые вы можете запросить в этом сервисе. Затем, когда приходят запросы, они проверяются и выполняются по этой схеме.
Тип языка
Сервисы GraphQL могут быть написаны на любом языке. Поскольку мы не можем полагаться на синтаксис определенного языка программирования, такого как JavaScript, чтобы говорить о схемах GraphQL, мы определим наш собственный простой язык. Мы будем использовать «язык схем GraphQL» — он похож на язык запросов и позволяет нам говорить о схемах GraphQL независимо от языка.
Типы объектов и поля
Самыми базовыми компонентами схемы GraphQL являются типы объектов, которые просто представляют тип объекта, который вы можете получить из своего сервиса, и его поля. На языке схемы GraphQL мы могли бы представить это так:
type Character {
name: String!
appearsIn: [Episode!]!
}
Язык довольно удобочитаемый, но давайте рассмотрим его, чтобы у нас был общий словарный запас:
Character — это тип объекта GraphQL, то есть тип с некоторыми полями. Большинство типов в вашей схеме будут объектными типами.
name и AppsIn — это поля для типа символов. Это означает, что name и visibleIn — единственные поля, которые могут появляться в любой части запроса GraphQL, работающего с типом Character.
String — это один из встроенных скалярных типов — это типы, которые разрешаются в один скалярный объект и не могут иметь подвыборки в запросе. Мы рассмотрим скалярные типы позже.
Нить! означает, что поле не может принимать значения NULL, а это означает, что служба GraphQL обещает всегда давать вам значение при запросе этого поля. На языке типов мы будем обозначать их восклицательным знаком.
[Эпизод!]! представляет массив объектов Episode. Поскольку он также не может принимать значения NULL, вы всегда можете ожидать массив (с нулем или более элементов) при запросе поля появления в. И начиная с Эпизода! также не может быть нулевым, вы всегда можете ожидать, что каждый элемент массива будет объектом Episode.
Теперь вы знаете, как выглядит объектный тип GraphQL и как читать основы языка типов GraphQL.
Аргументы
Каждое поле типа объекта GraphQL может иметь ноль или более аргументов, например поле длины ниже:
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
Все аргументы названы. В отличие от таких языков, как JavaScript и Python, где функции принимают список упорядоченных аргументов, все аргументы в GraphQL передаются по имени. В этом случае поле длины имеет один определенный аргумент, единицу измерения.
Аргументы могут быть как обязательными, так и необязательными. Когда аргумент является необязательным, мы можем определить значение по умолчанию — если аргумент единиц измерения не передан, по умолчанию он будет установлен в METER.
Типы запроса и мутации
Большинство типов в вашей схеме будут просто обычными типами объектов, но есть два типа, которые являются особыми в схеме:
schema {
query: Query
mutation: Mutation
}
Каждая служба GraphQL имеет тип запроса и может иметь или не иметь тип мутации. Эти типы такие же, как обычные типы объектов, но они особенные, поскольку определяют точку входа для каждого запроса GraphQL. Итак, если вы видите запрос, который выглядит так:
query {
hero {
name
}
droid(id: "2000") {
name
}
}
{
"data": {
"hero": {
"name": "R2-D2"
},
"droid": {
"name": "C-3PO"
}
}
}
Это означает, что сервис GraphQL должен иметь тип Query с полями hero и droid:
type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}
Мутации работают аналогичным образом — вы определяете поля для типа мутации, и они доступны как корневые поля мутации, которые вы можете вызывать в своем запросе.
Важно помнить, что помимо особого статуса «точки входа» в схему, типы Query и Mutation такие же, как и любой другой тип объекта GraphQL, и их поля работают точно так же.
Скалярные типы
Тип объекта GraphQL имеет имя и поля, но в какой-то момент эти поля должны разрешаться в некоторые конкретные данные. Вот где появляются скалярные типы: они представляют листья запроса.
В следующем запросе поля name и visibleIn будут преобразованы в скалярные типы:
{
hero {
name
appearsIn
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
Мы знаем это, потому что у этих полей нет подполей — они являются листьями запроса.
GraphQL поставляется с набором скалярных типов по умолчанию:
Int: 32-битное целое число со знаком.
Float: значение с плавающей запятой двойной точности со знаком.
Строка: последовательность символов UTF-8.
Логическое: истинно или ложно.
ID: скалярный тип ID представляет собой уникальный идентификатор, часто используемый для повторной выборки объекта или в качестве ключа для кеша. Тип ID сериализуется так же, как String; однако определение его как идентификатора означает, что он не предназначен для чтения человеком.
В большинстве реализаций службы GraphQL также есть способ указать собственные скалярные типы. Например, мы могли бы определить тип даты:
scalar Date
Затем наша реализация должна определить, как этот тип должен быть сериализован, десериализован и проверен. Например, вы можете указать, что тип даты всегда должен сериализоваться в целочисленную отметку времени, и ваш клиент должен знать, что этот формат следует ожидать для любых полей даты.
Типы перечисления
Типы перечислений, также называемые перечислениями, представляют собой особый вид скаляра, который ограничен определенным набором допустимых значений. Это позволяет:
Убедитесь, что любые аргументы этого типа являются одним из допустимых значений.
Сообщите через систему типов, что поле всегда будет одним из конечного набора значений.
Вот как может выглядеть определение перечисления на языке схемы GraphQL:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
Это означает, что везде, где мы используем тип Episode в нашей схеме, мы ожидаем, что это будет точно один из типов NEWHOPE, EMPIRE или JEDI.
Обратите внимание, что реализации службы GraphQL на разных языках будут иметь свой собственный способ работы с перечислениями, зависящий от языка. В языках, поддерживающих перечисления как первоклассных граждан, реализация может использовать это в своих интересах; в таком языке, как JavaScript, без поддержки перечисления, эти значения могут быть внутренне сопоставлены с набором целых чисел. Однако эти детали не передаются клиенту, который может работать исключительно в терминах строковых имен значений перечисления.
Списки и ненулевые значения
Типы объектов, скаляры и перечисления — это единственные типы типов, которые вы можете определить в GraphQL. Но когда вы используете типы в других частях схемы или в объявлениях переменных запроса, вы можете применять дополнительные модификаторы типов, влияющие на проверку этих значений. Давайте посмотрим на пример:
type Character {
name: String!
appearsIn: [Episode]!
}
Здесь мы используем тип String и помечаем его как Non-Null, добавляя восклицательный знак ! после имени типа. Это означает, что наш сервер всегда ожидает возврата ненулевого значения для этого поля, и если он в конечном итоге получит нулевое значение, это фактически вызовет ошибку выполнения GraphQL, сообщив клиенту, что что-то пошло не так.
Модификатор типа Non-Null также можно использовать при определении аргументов для поля, что приведет к тому, что сервер GraphQL вернет ошибку проверки, если в качестве этого аргумента будет передано нулевое значение, будь то в строке GraphQL или в переменных.
query DroidById($id: ID!) {
droid(id: $id) {
name
}
}
{
"errors": [
{
"message": "Variable \"$id\" of non-null type \"ID!\" must not be null.",
"locations": [
{
"line": 1,
"column": 17
}
]
}
]
}
{
"id": null
}
Списки работают аналогичным образом: мы можем использовать модификатор типа, чтобы пометить тип как список, что указывает на то, что это поле будет возвращать массив этого типа. На языке схем это обозначается заключением типа в квадратные скобки [ и ]. Это работает так же для аргументов, где шаг проверки ожидает массив для этого значения.
Модификаторы Non-Null и List можно комбинировать. Например, у вас может быть список ненулевых строк:
myField: [String!]
Это означает, что сам список может быть нулевым, но в нем не может быть пустых элементов. Например, в JSON:
myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error
Теперь предположим, что мы определили Non-Null List of Strings:
myField: [String]!
Это означает, что сам список не может быть нулевым, но может содержать нулевые значения:
myField: null // error
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid
Вы можете произвольно вкладывать любое количество модификаторов Non-Null и List в соответствии с вашими потребностями.
Интерфейсы
Как и многие системы типов, GraphQL поддерживает интерфейсы. Интерфейс — это абстрактный тип, который включает в себя определенный набор полей, которые тип должен включать для реализации интерфейса.
Например, у вас может быть интерфейс Character, представляющий любого персонажа из трилогии «Звездных войн»:
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
Это означает, что любой тип, который реализует Character, должен иметь именно эти поля с этими аргументами и возвращаемыми типами.
Например, вот некоторые типы, которые могут реализовывать Character:
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
Вы можете видеть, что оба этих типа имеют все поля из интерфейса персонажа, но также содержат дополнительные поля, общие кредиты, звездолеты и первичную функцию, которые специфичны для этого конкретного типа персонажа.
Интерфейсы полезны, когда вы хотите вернуть объект или набор объектов, но они могут быть нескольких разных типов.
Например, обратите внимание, что следующий запрос выдает ошибку:
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
primaryFunction
}
}
{
"errors": [
{
"message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}
{
"ep": "JEDI"
}
Поле героя возвращает тип Персонаж, что означает, что это может быть либо Человек, либо Дроид, в зависимости от аргумента эпизода. В приведенном выше запросе вы можете запрашивать только поля, которые существуют в интерфейсе символов, который не включает в себя функцию primaryFunction.
Чтобы запросить поле для определенного типа объекта, вам нужно использовать встроенный фрагмент:
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
{
"ep": "JEDI"
}
Типы союзов
Типы объединения очень похожи на интерфейсы, но они не могут указывать какие-либо общие поля между типами.
union SearchResult = Human | Droid | Starship
Везде, где мы возвращаем тип SearchResult в нашей схеме, мы можем получить человека, дроида или звездолет. Обратите внимание, что члены типа объединения должны быть конкретными типами объектов; вы не можете создать тип объединения из интерфейсов или других объединений.
В этом случае, если вы запрашиваете поле, которое возвращает тип объединения SearchResult, вам нужно использовать встроенный фрагмент, чтобы вообще иметь возможность запрашивать любые поля:
{
search(text: "an") {
__typename
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
... on Starship {
name
length
}
}
}
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo",
"height": 1.8
},
{
"__typename": "Human",
"name": "Leia Organa",
"height": 1.5
},
{
"__typename": "Starship",
"name": "TIE Advanced x1",
"length": 9.2
}
]
}
}
Поле __typename преобразуется в строку, которая позволяет отличать разные типы данных друг от друга на клиенте.
Кроме того, в этом случае, поскольку Человек и Дроид имеют общий интерфейс (Персонаж), вы можете запрашивать их общие поля в одном месте, а не повторять одни и те же поля для нескольких типов:
{
search(text: "an") {
__typename
... on Character {
name
}
... on Human {
height
}
... on Droid {
primaryFunction
}
... on Starship {
name
length
}
}
}
Обратите внимание, что имя по-прежнему указано в Starship, иначе оно не будет отображаться в результатах, учитывая, что Starship не является персонажем!
Типы ввода
До сих пор мы говорили только о передаче скалярных значений, таких как перечисления или строки, в качестве аргументов в поле. Но вы также можете легко проходить сложные объекты. Это особенно ценно в случае мутаций, когда вы можете захотеть передать весь объект для создания. В языке схем GraphQL типы ввода выглядят точно так же, как и обычные типы объектов, но с ключевым словом input вместо type:
input ReviewInput {
stars: Int!
commentary: String
}
Вот как вы можете использовать тип входного объекта в мутации:
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
Поля типа входного объекта могут сами ссылаться на типы входных объектов, но вы не можете смешивать входные и выходные типы в своей схеме. Типы входных объектов также не могут иметь аргументов в своих полях.
Проверка
Используя систему типов, можно заранее определить, является ли запрос GraphQL допустимым или нет. Это позволяет серверам и клиентам эффективно информировать разработчиков о создании недопустимого запроса, не полагаясь на проверки во время выполнения.
Для нашего примера «Звездных войн» файл starWarsValidation-test.ts содержит ряд запросов, демонстрирующих различные недопустимости, и представляет собой тестовый файл, который можно запустить для проверки валидатора эталонной реализации.
Для начала возьмем сложный допустимый запрос. Это вложенный запрос, похожий на пример из предыдущего раздела, но с дублированными полями, вынесенными во фрагмент:
{
hero {
...NameAndAppearances
friends {
...NameAndAppearances
friends {
...NameAndAppearances
}
}
}
}
fragment NameAndAppearances on Character {
name
appearsIn
}
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
},
{
"name": "Leia Organa",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
},
{
"name": "C-3PO",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
},
{
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
]
},
{
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
},
{
"name": "Leia Organa",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
},
{
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
]
},
{
"name": "Leia Organa",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
},
{
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
},
{
"name": "C-3PO",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
},
{
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
]
}
]
}
}
}
И этот запрос действителен. Давайте посмотрим на некоторые недопустимые запросы...
Фрагмент не может ссылаться сам на себя или создавать цикл, так как это может привести к неограниченному результату! Вот тот же запрос выше, но без явных трех уровней вложенности:
{
hero {
...NameAndAppearancesAndFriends
}
}
fragment NameAndAppearancesAndFriends on Character {
name
appearsIn
friends {
...NameAndAppearancesAndFriends
}
}
{
"errors": [
{
"message": "Cannot spread fragment \"NameAndAppearancesAndFriends\" within itself.",
"locations": [
{
"line": 11,
"column": 5
}
]
}
]
}
Когда мы запрашиваем поля, мы должны запрашивать поле, которое существует в данном типе. Так как герой возвращает символ, мы должны запросить поле для персонажа. Этот тип не имеет поля FavoriteSpaceship, поэтому этот запрос недействителен:
# INVALID: favoriteSpaceship does not exist on Character
{
hero {
favoriteSpaceship
}
}
{
"errors": [
{
"message": "Cannot query field \"favoriteSpaceship\" on type \"Character\".",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}
Всякий раз, когда мы запрашиваем поле, и оно возвращает что-то отличное от скаляра или перечисления, нам нужно указать, какие данные мы хотим получить из поля. Hero возвращает Character, и мы запрашивали для него такие поля, как name и visibleIn; если мы опустим это, запрос будет недействительным:
# INVALID: hero is not a scalar, so fields are needed
{
hero
}
{
"errors": [
{
"message": "Field \"hero\" of type \"Character\" must have a selection of subfields. Did you mean \"hero { ... }\"?",
"locations": [
{
"line": 3,
"column": 3
}
]
}
]
}
Точно так же, если поле является скалярным, нет смысла запрашивать дополнительные поля для него, и это сделает запрос недействительным:
# INVALID: name is a scalar, so fields are not permitted
{
hero {
name {
firstCharacterOfName
}
}
}
{
"errors": [
{
"message": "Field \"name\" must not have a selection since type \"String!\" has no subfields.",
"locations": [
{
"line": 4,
"column": 10
}
]
}
]
}
Ранее было отмечено, что запрос может запрашивать только поля рассматриваемого типа; когда мы запрашиваем героя, который возвращает символ, мы можем запрашивать только те поля, которые существуют для персонажа. Что произойдет, если мы захотим запросить основную функцию R2-D2?
# INVALID: primaryFunction does not exist on Character
{
hero {
name
primaryFunction
}
}
{
"errors": [
{
"message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
"locations": [
{
"line": 5,
"column": 5
}
]
}
]
}
Этот запрос недействителен, так как primaryFunction не является полем в Character. Нам нужен какой-то способ указать, что мы хотим получить основную функцию, если персонаж является дроидом, и игнорировать это поле в противном случае. Для этого мы можем использовать фрагменты, которые мы представили ранее. Настроив фрагмент, определенный в Droid, и включив его, мы гарантируем, что запрашиваем только основную функцию там, где она определена.
{
hero {
name
...DroidFields
}
}
fragment DroidFields on Droid {
primaryFunction
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
Этот запрос действителен, но немного многословен; именованные фрагменты были полезны выше, когда мы использовали их несколько раз, но мы используем этот только один раз. Вместо использования именованного фрагмента мы можем использовать встроенный фрагмент; это по-прежнему позволяет нам указать тип, к которому мы обращаемся, но без именования отдельного фрагмента:
{
hero {
name
... on Droid {
primaryFunction
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
Это только поверхностно коснулось системы валидации; существует ряд правил проверки, чтобы гарантировать, что запрос GraphQL является семантически значимым. Спецификация более подробно описывает эту тему в разделе «Проверка», а каталог проверки в GraphQL.js содержит код, реализующий соответствующий спецификации валидатор GraphQL.
Исполнение
После проверки запрос GraphQL выполняется сервером GraphQL, который возвращает результат, отражающий форму запрошенного запроса, обычно в формате JSON.
GraphQL не может выполнить запрос без системы типов, давайте воспользуемся примером системы типов, чтобы проиллюстрировать выполнение запроса. Это часть той же системы типов, которая используется во всех примерах в этих статьях:
type Query {
human(id: ID!): Human
}
type Human {
name: String
appearsIn: [Episode]
starships: [Starship]
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
type Starship {
name: String
}
Чтобы описать, что происходит при выполнении запроса, давайте рассмотрим пример.
human(id: 1002) {
name
appearsIn
starships {
name
}
}
}
{
"data": {
"human": {
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"starships": [
{
"name": "Millenium Falcon"
},
{
"name": "Imperial shuttle"
}
]
}
}
}
Вы можете думать о каждом поле в запросе GraphQL как о функции или методе предыдущего типа, который возвращает следующий тип. На самом деле именно так работает GraphQL. Каждое поле каждого типа поддерживается функцией, называемой преобразователем, которая предоставляется разработчиком сервера GraphQL. Когда поле выполняется, вызывается соответствующий преобразователь для получения следующего значения.
Если поле выдает скалярное значение, такое как строка или число, выполнение завершается. Однако, если поле создает значение объекта, тогда запрос будет содержать другой выбор полей, которые применяются к этому объекту. Это продолжается до тех пор, пока не будут достигнуты скалярные значения. Запросы GraphQL всегда заканчиваются скалярными значениями.
Корневые поля и преобразователи
На верхнем уровне каждого сервера GraphQL находится тип, представляющий все возможные точки входа в GraphQL API, его часто называют корневым типом или типом запроса.
В этом примере наш тип Query предоставляет поле с именем human, которое принимает идентификатор аргумента. Функция разрешения для этого поля, вероятно, обращается к базе данных, а затем создает и возвращает объект Human.
Query: {
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
}
Этот пример написан на JavaScript, однако серверы GraphQL можно создавать на разных языках. Функция разрешения получает четыре аргумента:
obj Предыдущий объект, который для поля в корневом запросе часто не используется.
args Аргументы, предоставляемые полю в запросе GraphQL.
контекст Значение, предоставляемое каждому распознавателю и содержащее важную контекстную информацию, такую как текущий пользователь, вошедший в систему, или доступ к базе данных.
info Значение, которое содержит специфичную для поля информацию, относящуюся к текущему запросу, а также сведения о схеме. Дополнительные сведения см. также в типе GraphQLResolveInfo.
Асинхронные преобразователи
Давайте подробнее рассмотрим, что происходит в этой функции преобразователя.
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
Контекст используется для предоставления доступа к базе данных, которая используется для загрузки данных для пользователя по идентификатору, указанному в качестве аргумента в запросе GraphQL. Поскольку загрузка из базы данных является асинхронной операцией, возвращается Promise. В JavaScript промисы используются для работы с асинхронными значениями, но та же концепция существует во многих языках, часто называемых фьючерсами, задачами или отложенными. Когда база данных возвращается, мы можем создать и вернуть новый объект Human.
Обратите внимание, что хотя функция распознавателя должна знать об обещаниях, запрос GraphQL этого не делает. Он просто ожидает, что человеческое поле вернет что-то, у чего затем сможет запросить имя. Во время выполнения GraphQL будет ждать завершения промисов, фьючерсов и задач, прежде чем продолжить, и будет делать это с оптимальным параллелизмом.
Тривиальные преобразователи
Теперь, когда объект Human доступен, выполнение GraphQL может продолжаться с запрошенными для него полями.
Human: {
name(obj, args, context, info) {
return obj.name
}
}
Сервер GraphQL работает на основе системы типов, которая используется для определения того, что делать дальше. Еще до того, как поле человека вернет что-либо, GraphQL знает, что следующим шагом будет разрешение полей типа Human, поскольку система типов сообщает ему, что поле человека вернет Human.
Разрешение имени в этом случае очень прямолинейно. Вызывается функция преобразования имен, и аргумент obj представляет собой новый объект Human, возвращенный из предыдущего поля. В этом случае мы ожидаем, что объект Human будет иметь свойство имени, которое мы можем прочитать и вернуть напрямую.
На самом деле, многие библиотеки GraphQL позволяют вам так просто не использовать распознаватели и просто предполагают, что, если распознаватель не предоставлен для поля, свойство с тем же именем должно быть прочитано и возвращено.
Скалярное принуждение
Пока разрешается поле имени, поля AppsIn и Starships могут обрабатываться одновременно. Поле AppInsIn также может иметь тривиальный преобразователь, но давайте рассмотрим его подробнее:
Human: {
appearsIn(obj) {
return obj.appearsIn // returns [ 4, 5, 6 ]
}
}
Заметьте, что наша система типов с утверждениями existsIn будет возвращать значения Enum с известными значениями, однако эта функция возвращает числа! Действительно, если мы посмотрим на результат, мы увидим, что возвращаются соответствующие значения Enum. В чем дело?
Это пример скалярного принуждения. Система типов знает, чего ожидать, и преобразует значения, возвращаемые функцией распознавателя, во что-то, что поддерживает контракт API. В этом случае на нашем сервере может быть определен Enum, который использует такие числа, как 4, 5 и 6, но представляет их как значения Enum в системе типов GraphQL.
Список распознавателей
Мы уже видели кое-что из того, что происходит, когда поле возвращает список вещей с полем visibleIn выше. Он вернул список значений перечисления, и, поскольку этого ожидала система типов, каждый элемент в списке был приведен к соответствующему значению перечисления. Что произойдет, когда поле звездолетов будет разрешено?
Human: {
starships(obj, args, context, info) {
return obj.starshipIDs.map(
id => context.db.loadStarshipByID(id).then(
shipData => new Starship(shipData)
)
)
}
}
Преобразователь для этого поля не просто возвращает промис, он возвращает список промисов. У объекта Human был список идентификаторов кораблей, которыми они управляли, но нам нужно загрузить все эти идентификаторы, чтобы получить настоящие объекты Starship.
GraphQL будет ожидать всех этих промисов одновременно, прежде чем продолжить, и, когда останется список объектов, он будет одновременно продолжать загружать поле имени для каждого из этих элементов.
Получение результата
По мере разрешения каждого поля результирующее значение помещается в карту «ключ-значение» с именем поля (или псевдонимом) в качестве ключа и разрешенным значением в качестве значения. Это продолжается от нижних конечных полей запроса до исходного поля в корневом типе запроса. В совокупности они создают структуру, которая отражает исходный запрос, который затем может быть отправлен (обычно в формате JSON) клиенту, который его запросил.
Давайте в последний раз взглянем на исходный запрос, чтобы увидеть, как все эти разрешающие функции дают результат:
{
human(id: 1002) {
name
appearsIn
starships {
name
}
}
}
{
"data": {
"human": {
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"starships": [
{
"name": "Millenium Falcon"
},
{
"name": "Imperial shuttle"
}
]
}
}
}
Самоанализ
Часто бывает полезно запросить у схемы GraphQL информацию о том, какие запросы она поддерживает. GraphQL позволяет нам делать это с помощью системы самоанализа!
Для нашего примера «Звездных войн» файл starWarsIntrospection-test.ts содержит ряд запросов, демонстрирующих систему самоанализа, и представляет собой тестовый файл, который можно запустить для проверки системы самоанализа эталонной реализации.
Мы разработали систему типов, поэтому мы знаем, какие типы доступны, но если мы этого не сделали, мы можем запросить GraphQL, запросив поле __schema, всегда доступное для корневого типа запроса. Давайте сделаем это сейчас и спросим, какие типы доступны.
{
__schema {
types {
name
}
}
}
{
"data": {
"__schema": {
"types": [
{
"name": "Query"
},
{
"name": "String"
},
{
"name": "ID"
},
{
"name": "Mutation"
},
{
"name": "Episode"
},
{
"name": "Character"
},
{
"name": "Int"
},
{
"name": "LengthUnit"
},
{
"name": "Human"
},
{
"name": "Float"
},
{
"name": "Droid"
},
{
"name": "FriendsConnection"
},
{
"name": "FriendsEdge"
},
{
"name": "PageInfo"
},
{
"name": "Boolean"
},
{
"name": "Review"
},
{
"name": "ReviewInput"
},
{
"name": "Starship"
},
{
"name": "SearchResult"
},
{
"name": "__Schema"
},
{
"name": "__Type"
},
{
"name": "__TypeKind"
},
{
"name": "__Field"
},
{
"name": "__InputValue"
},
{
"name": "__EnumValue"
},
{
"name": "__Directive"
},
{
"name": "__DirectiveLocation"
}
]
}
}
}
Ого, сколько типов! Кто они такие? Сгруппируем их:
Query, Character, Human, Episode, Droid — это те, которые мы определили в нашей системе типов.
String, Boolean — это встроенные скаляры, предоставляемые системой типов.
__Schema, __Type, __TypeKind, __Field, __InputValue, __EnumValue, __Directive — всем им предшествует двойное подчеркивание, указывающее, что они являются частью системы самоанализа.
Теперь давайте попробуем найти хорошее место для начала изучения доступных запросов. Когда мы проектировали нашу систему типов, мы указали, с какого типа будут начинаться все запросы; давайте спросим об этом систему самоанализа!
{
__schema {
queryType {
name
}
}
}
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
}
}
}
}
И это соответствует тому, что мы сказали в разделе о системе типов, что мы начнем с типа Query! Обратите внимание, что именование здесь было просто условным; мы могли бы назвать наш тип Query как угодно, и он все равно был бы возвращен здесь, если бы мы указали, что это начальный тип для запросов. Однако назвать его Query — полезное соглашение.
Часто бывает полезно изучить один конкретный тип. Давайте посмотрим на тип Droid:
{
__type(name: "Droid") {
name
}
}
{
"data": {
"__type": {
"name": "Droid"
}
}
}
Что, если мы хотим узнать больше о Droid? Например, это интерфейс или объект?
{
__type(name: "Droid") {
name
kind
}
}
{
"data": {
"__type": {
"name": "Droid",
"kind": "OBJECT"
}
}
}
kind возвращает перечисление __TypeKind, одним из значений которого является OBJECT. Если бы вместо этого мы спросили о Character, то обнаружили бы, что это интерфейс:
{
__type(name: "Character") {
name
kind
}
}
{
"data": {
"__type": {
"name": "Character",
"kind": "INTERFACE"
}
}
}
Объекту полезно знать, какие поля доступны, поэтому давайте спросим систему самоанализа о Droid:
{
__type(name: "Droid") {
name
fields {
name
type {
name
kind
}
}
}
}
{
"data": {
"__type": {
"name": "Droid",
"fields": [
{
"name": "id",
"type": {
"name": null,
"kind": "NON_NULL"
}
},
{
"name": "name",
"type": {
"name": null,
"kind": "NON_NULL"
}
},
{
"name": "friends",
"type": {
"name": null,
"kind": "LIST"
}
},
{
"name": "friendsConnection",
"type": {
"name": null,
"kind": "NON_NULL"
}
},
{
"name": "appearsIn",
"type": {
"name": null,
"kind": "NON_NULL"
}
},
{
"name": "primaryFunction",
"type": {
"name": "String",
"kind": "SCALAR"
}
}
]
}
}
}
Это наши поля, которые мы определили на Droid!
id выглядит немного странно, у него нет имени для типа. Это потому, что это тип "обертки" типа NON_NULL. Если бы мы запросили ofType для типа этого поля, мы бы нашли там тип ID, говорящий нам, что это ненулевой ID.
Точно так же и friends, и visibleIn не имеют имени, так как они являются оболочкой типа LIST. Мы можем запросить ofType для этих типов, что скажет нам, что это за списки.
{
__type(name: "Droid") {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}
{
"data": {
"__type": {
"name": "Droid",
"fields": [
{
"name": "id",
"type": {
"name": null,
"kind": "NON_NULL",
"ofType": {
"name": "ID",
"kind": "SCALAR"
}
}
},
{
"name": "name",
"type": {
"name": null,
"kind": "NON_NULL",
"ofType": {
"name": "String",
"kind": "SCALAR"
}
}
},
{
"name": "friends",
"type": {
"name": null,
"kind": "LIST",
"ofType": {
"name": "Character",
"kind": "INTERFACE"
}
}
},
{
"name": "friendsConnection",
"type": {
"name": null,
"kind": "NON_NULL",
"ofType": {
"name": "FriendsConnection",
"kind": "OBJECT"
}
}
},
{
"name": "appearsIn",
"type": {
"name": null,
"kind": "NON_NULL",
"ofType": {
"name": null,
"kind": "LIST"
}
}
},
{
"name": "primaryFunction",
"type": {
"name": "String",
"kind": "SCALAR",
"ofType": null
}
}
]
}
}
}
Давайте закончим характеристикой системы самоанализа, особенно полезной для инструментов; давайте запросим у системы документацию!
{
__type(name: "Droid") {
name
description
}
}
{
"data": {
"__type": {
"name": "Droid",
"description": null
}
}
}
Таким образом, мы можем получить доступ к документации о системе типов, используя самоанализ, и создать браузеры документации или богатый опыт IDE.