GraphQL
- это язык запросов для API
и серверного окружения, предназначенный для выполнения запросов с помощью системы типов, определенных для данных. Сервис GraphQL
создается посредством определения типов и их полей и создания функций для каждого поля каждого типа.
- Поля
- Аргументы
- Синонимы
- Фрагменты
- Название операции
- Переменные
- Директивы
- Мутации
- Встроенные фрагменты
- Поля с метаданными
В простейшем случае, GraphQL позволяет запрашивать (получать) поля объекта:
{
hero {
name
}
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
С таким же успехом можно получать поля вложенных объектов:
{
hero {
name
# Запросы могут иметь комментарии
friends {
name
}
}
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
GraphQL позволяет передавать полям объектов аргументы в момент их запроса:
{
human(id: '123') {
name
height
}
}
// вывод
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 1.72
}
}
}
Это, в частности, позволяет передавать аргументы нескольким полям одновременно, что может существенно сократить количество запросов к серверу:
{
human(id: '123') {
name
height(unit: FOOT)
}
}
// вывод
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 5.6430448
}
}
}
Здесь мы имеем дело с перечислением (enumeration type): значение длины может быть выражено либо в метрах (METER), либо в футах (FOOT). Перечисления могут расширяться сервером.
Синонимы (aliases) позволяют использовать одинаковые названия полей с разными аргументами (переименовывать возвращаемый результат):
{
# Синоним: название поля
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
// вывод
{
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}
Фрагменты позволяют определять набор полей для повторного использования в запросах:
{
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
, за которым следует название операции:
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
Типом операции может быть запрос (query), мутация (mutation) или подписка (subscribe).
Для того, чтобы иметь возможность работать с переменными, необходимо сделать следующее:
- Заменить статическое значение в запросе на
$variableName
- Определить
$variableName
в качестве параметра запроса - Передать
variableName: value
в транспортируемом формате (обычно, таким форматом являетсяJSON
)
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
// переменная
{
"episode": "JEDI"
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
$episode: Episode
означает, что переменная episode
должна соответствовать типу Episode
. !
после типа означает, что переменная является обязательной (в приведенном примере переменная является опциональной).
query HeroNameAndFriends($episode: Episode = JEDI) {
hero(episode: $episode) {
name
friends {
name
}
}
}
Директивы (directives) позволяют конструировать запросы динамически:
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends)
}
}
// переменные
{
"episode": "JEDI",
"withFriends": false
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
На сегодняшний день GraphQL предоставляет 2 директивы:
@include(if: Boolean)
- включение полей при удовлетворении условия@skip(if: Boolean)
- пропуск полей
Директивы могут расширяться сервером.
Если запросы только возвращают данные, то мутации позволяют эти данные изменять. В ответ на мутацию возвращается новое состояние объекта:
mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {
createReview(episode: $episode, review: $review) {
stars
comment
}
}
// переменные
{
"episode": "JEDI",
"review": {
"stars": 5,
"comment": "Это великий фильм!"
}
}
// вывод
{
"data": {
"createReview": {
"stars": 5,
"comment": "Это великий фильм!"
}
}
}
Обратите внимание, что типом $review
является специальный тип объекта - входной (или входящий) объект (или данные) (input object type).
Как и запросы, мутации могут содержать несколько полей. Основное отличие между ними состоит в том, что запросы выполняются параллельно, а мутации - последовательно. Это объясняется тем, что первая мутация должна завершиться до начала следующей, так как обе мутации могут изменять одни и те же данные.
Схемы GraphQL
позволяют определять интерфейсы (interfaces) и альтернативные типы (union types). В случае, когда в ответ на запрос возвращается интерфейс или альтернативный тип, для доступа к данным нижележащего конкретного типа (concrete type) используются встроенные фрагменты:
query HeroForEpisode($episode: Episode!) {
hero(episode: $episode) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
// переменная
{
"episode": "JEDI"
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
В данном запросе поле hero
возвращает тип Character
, который может быть Human
или Droid
в зависимости от аргумента episode
. Мы можем запрашивать только те поля, которые существуют в интерфейсе Character
, такие как name
.
Для запроса поля конкретного типа используются встроенные фрагменты c условием. Поскольку первый аргумент помечен как ... on Droid
, поле primaryFunction
будет выполняться, только если Character
, возвращенный из hero
, будет иметь тип Droid
. Тоже самое справедливо для поля height
типа Human
.
Мета-поле __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"
}
]
}
}
- Система типов
- Типы и поля объектов
- Аргументы
- Запрос и мутация
- Скалярные типы
- Перечисления
- Списки и ненулевые значения
- Интерфейсы
- Альтернативные типы
- Входящие данные
В следующем примере мы начинаем со специального объекта root
, выбираем его поле hero
, которое является объектом, затем выбираем поля name
и appearsIn
этого объекта:
{
hero {
name
appearsIn
}
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
Каждый GraphQL-сервис определяет набор типов, описывающих данные, которые можно запрашивать. При получении запросов, они проверяются и выполняются с помощью схемы. При этом, сервис может быть реализован на любом языке, а не только на JavaScript
.
Основными компонентами схем являются объекты (object types):
type Character {
name: String!
appearsIn: [Episode!]!
}
Character
- это объектный тип, содержащий поля. Большая часть типов любой схемы является объектамиname
иappearsIn
- поля типаCharacter
. Только эти поля можно запрашиватьString
- один из встроенных скалярных типов (scalar types). Скалярные типы не могут содержать полейString!
- означает, что поле является ненулевым, не может иметь нулевое значение, т.е. является обязательным[Episode!]!
- обязательный (ненулевой) массив, содержащий объекты с обязательным типомEpisode
Каждое поле может иметь аргументы:
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
Все аргументы являются именованными, т.е. аргументы передаются по названию. Аргументы могут быть обязательными или опциональными. В последнем случае можно определять дефолтные значения.
В схеме существует два специальных типа:
schema {
query: Query
mutation: Mutation
}
Каждый сервис имеет тип query
и может иметь тип mutation
. Эти типы являются входными точками для запросов. Такой запрос:
query {
hero {
name
}
droid(id: '123') {
name
}
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2"
},
"droid": {
"name": "C-3PO"
}
}
}
Означает, что сервис должен иметь тип Query
с полями hero
и droid
:
type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}
Мутации работают похожим образом.
Объект имеет название и поля, но в определенный момент эти поля должны разрешиться в конкретные данные - скалярные типы:
{
hero {
name
appearsIn
}
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
Дефолтные скалярные типы:
Int
- 32-битное целое число со знакомFloat
- число двойной точности со знакомString
- строка, последовательность символовUTF-8
Boolean
-true
илиfalse
ID
- уникальный идентификатор, используемый для получения объектов, а также в качестве ключей для кэша
Допускается определять кастомные скалярные типы:
scalar Date
Enums
- специальный скалярный тип, набор допустимых значений:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
В GraphQL
можно определять объекты, скалярные типы и перечисления. Однако, при использовании этих типов в других частях схемы или при определении переменных запроса можно применять дополнительные модификаторы, выполняющие валидацию этих значений:
type Character {
name: String!
appearsIn: [Episode]!
}
Символ !
после названия типа означает, что данное поле является обязательным. При отсутствии значения обязательного поля выбрасывается исключение:
query DroidById($id: ID!) {
droid(id: $id) {
name
}
}
// переменная
{
"id": null
}
// вывод
{
"errors": [
{
"message": "Variable \"$id\" of non-null type \"ID!\" must not be null.",
"locations": [
{
"line": 1,
"column": 17
}
]
}
]
}
Списки работают похожим образом. Мы используем модификатор []
для пометки типа в качестве List
. Это означает, что поле должно возвращать массив определенных типов. Модификаторы ненулевого значения и списка можно комбинировать:
myField: [String!]
Это означает, что список сам по себе может быть пустым, но не может содержать нулевые значения:
myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error
Определим ненулевой список строк:
myField: [String]!
Это означает, что список не может быть пустым, но может содержать нулевые значения:
myField: null // error
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid
Интерфейс (interface) - это абстрактный тип, включающий набор полей, которые должен содержать тип для реализации данного интерфейса.
Вот пример интерфейса Character
, представляющий любого персонажа из трилогии "Звездные войны":
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
Любой тип, реализующий данный интерфейс должен содержать указанные поля с указанными аргументами и возвращаемыми типами. Вот несколько типов, реализующих 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($episode: Episode!) {
hero(episode: $episode) {
name
primaryFunction
}
}
// переменная
{
"episode": "JEDI"
}
// вывод
{
"errors": [
{
"message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}
Ошибка возникает из-за того, что интерфейс не имеет поля primaryFunction
. Для запроса поля определенного объекта следует использовать встроенный фрагмент:
query HeroForEpisode($episode: Episode!) {
hero(episode: $episode) {
name
... on Droid {
primaryFunction
}
}
}
// переменная
{
"episode": "JEDI"
}
// вывод
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
Альтернативные типы (union types) похожи на интерфейсы, но они не определяют общие поля типов:
union SearchResult = Human | Droid | Starship
При запросе типа SearchResult
, мы можем получить Human
, Droid
или Starship
. Обратите внимание: мы не можем создавать альтернативные interface
или альтернативные union
.
При запросе, который разрешается типом 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
возвращает String
, позволяющую разделять типы данных на клиенте.
Поскольку Human
и Droid
имеют общий интерфейс (Character
), мы можем запрашивать их общие поля в одном месте:
{
search(text: "an") {
__typename
... on Character {
name
}
... on Human {
height
}
... on Droid {
primaryFunction
}
... on Starship {
name
length
}
}
}
Входящие (или входные) данные (input types) позволяют передавать запросам и мутациям сложные объекты в качестве аргументов:
input ReviewInput {
stars: Int!
comment: String
}
Вот как можно использовать этот тип в мутации:
mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {
createReview(episode: $episode, review: $review) {
stars
comment
}
}
// переменные
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
// вывод
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
Поля входных данных могут ссылаться на другие входные данные, но смешивать типы нельзя. Входные данные также не могут принимать аргументы.
Использование системы типов позволяет предопределять валидность запросов. Например, такой запрос будет валидным:
{
hero {
...NameAndAppearances
friends {
...NameAndAppearances
friends {
...NameAndAppearances
}
}
}
}
fragment NameAndAppearances on Character {
name
appearsIn
}
Фрагмент не может ссылаться на самого себя или создавать цикл. Поэтому такой запрос будет невалидным:
{
hero {
...NameAndAppearancesAndFriends
}
}
fragment NameAndAppearancesAndFriends on Character {
name
appearsIn
friends {
// здесь создается цикл
...NameAndAppearancesAndFriends
}
}
Очевидно, что мы можем запрашивать только существующие поля. В следующем примере запрашиваемого поля не существует:
{
hero {
// такого поля не существует
favoriteSpaceship
}
}
Когда запрашиваемое поле возвращает объект, мы должны определить, какие данные мы хотим получить из этого поля, поэтому такой запрос будет невалидным:
{
hero
}
Если поле является скалярным, запрос дополнительных полей приведет к ошибке:
{
hero {
// name является скалярным значением
name {
firstCharacterOfName
}
}
}
Ранее было отмечено, что мы можем запрашивать только те поля, которые существуют в типе. Когда мы запрашиваем hero
, возвращающее Character
, мы можем запрашивать только поля Character
. Что случится, если мы запросим основную функцию R2-D2:
{
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
. Нам необходимо определить, что мы хотим получать 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"
}
}
}
- Корневые поля и резолверы
- Асинхронные резолверы
- Обычные резолверы
- Приведение к скалярным значениям
- Резолверы списка
- Результат
После валидации, запрос выполняется. Сервер возвращает результат, соответствующий форме (shape) запроса, обычно, в формате JSON
.
Выполнение запроса предполагает наличие системы типов:
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: '123') {
name
appearsIn
starships {
name
}
}
}
// вывод
{
"data": {
"human": {
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"starships": [
{
"name": "Millenium Falcon"
},
{
"name": "Imperial shuttle"
}
]
}
}
}
По сути, каждое поле - это функция или метод предыдущего типа, которая возвращает следующий тип. Каждому полю соответствует функция - resolver
, которая вызывается при выполнении поля и возвращает новое значение.
Если поле возвращает скалярное значение, например, строку или число, его выполнение завершается. В противном случае, выполняются вложенные поля (следующая группа полей). И так до тех пор, пока каждое поле не разрешится скалярным значением.
На верхнем уровне любого GraphQL-сервера находится тип, представляющий все входные точки API. Он называется Root
или Query
.
В следующем примере Query
предоставляет поле human
, принимающее аргумент id
. Резолвер (функция) получает доступ к базе данных и возвращает объект Human
:
Query: {
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
}
obj
- предыдущий объект (используется редко)args
- переданные аргументыcontext
- контекст, значение, предоставляемое каждому резолверу, содержащее важную информацию, например, текущего авторизованного пользователя или доступ к базе данныхinfo
- значение, содержащее специфическую для запроса информацию, а также детали схемы
В приведенном выше примере context
используется для предоставления доступа к базе данных, что позволяет получить данные пользователя по id
, переданному в качестве аргумента в запросе. Поскольку получение данных из БД - это асинхронная операция, возвращается промис. После получения данных создается и возвращается новый объект Human
.
Задача по правильной обработке промисов возлагается на резолвера. В свою очередь, запрос просто ожидает получить поле human
для того, чтобы вернуть значение его поля name
. В процессе выполнения запроса GraphQL
по умолчанию ожидает завершения (разрешения) всех промисов и других асинхронных операций.
После того, как объект Human
стал доступен, GraphQL
может продолжить выполнение запроса:
Human: {
name(obj, args, context, info) {
return obj.name
}
}
Система типов позволяет серверу определить, что делать дальше. Еще до того, как поле human
вернуло какое-либо значение, GraphQL
знал, что следующим шагом будет разрешение полей типа Human
.
В данном случае получение имени - легкая задача. Резолвер вызывается с аргументом obj
, который является объектом new Human
, возвращенным предыдущим полем. Мы ожидаем, что у этого объекта имеется свойство name
, которое можно прочитать и вернуть.
Многие библиотеки GraphQL
выполняют такие примитивные резолверы автоматически.
Рассмотрим примитивный резолвер поля appearsIn
:
Human: {
appearsIn(obj) {
return obj.appearsIn // возвращается [ 4, 5, 6 ]
}
}
Наша система типов определяет, что appearsIn
возвращает перечисление. Вместо этого возвращается массив чисел. Что происходит?
Здесь имеет место приведение к скалярным значениям. Система типов преобразует значения, возвращаемые резолвером, в то, что соответствует контракту API (API contract). В данном случае числа 4, 5 и 6 предназначены для внутреннего использования сервером, каждое число представляет тот или иной элемент перечисления.
Итак, в случае с appearsIn
возвращается список чисел, разрешающихся соответствующим значением перечисления. Что происходит в случае с полем starships
?
Human: {
starships(obj, args, context, info) {
return obj.starshipsIDs.map(
id => context.db.loadStarshipByID(id).then(
shipData => new Starship(shipData)
)
)
}
}
Резолвер данного поля возвращает не один, а несколько (список) промисов. Объект Human
содержит список идентификаторов пилотируемых Starship
. Все эти id
нужно загрузить для получения соответствующих объектов.
GraphQL
будет ожидать разрешения всех промисов перед тем, как продолжить выполнение запроса, затем загрузит поле name
каждого элемента.
После разрешения всех полей результаты помещаются в пары ключ/значение, где ключ - это название поле (или его синоним), а значение - разрешенное значение этого поля. Таким образом, формируется зеркальная структура запроса, которая отправляется (как правило, в формате JSON
) клиенту.
{
human(id: '123') {
name
appearsIn
starships {
name
}
}
}
// вывод
{
"data": {
"human": {
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"starships": [
{
"name": "Millenium Falcon"
},
{
"name": "Imperial shuttle"
}
]
}
}
}
Интроспекция (introspection) позволяет получать информацию о запросах, поддерживаемых схемой. Например, доступные типы можно получить через поле __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"
}
}
}
}
Как нам получить информацию о конкретном типе?
{
__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"
}
}
}
Как нам получить информацию о полях объекта?
{
__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"
}
}
]
}
}
}
id
выглядит странно, оно не имеет названия для типа. Это обясняется тем, что данное поле является "оберткой" для NON_NULL
. Если мы запросим ofType
этого поля, то получим тип ID
.
Аналогичным образом friends
и appearsIn
являются обертками для LIST
.
{
__type(name: 'Droid') {
name
friends {
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": "An autonomous mechanical character in the Star Wars universe"
}
}
}