Перейти к содержанию

Валидация

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

В нашем примере с Star Wars файл starWarsValidation-test.js содержит запросы, демонстрирующие различные ошибки, и является тестовым файлом, который может быть запущен для осуществления контроля валидатора имплементации.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  hero {
    ...NameAndAppearances
    friends {
      ...NameAndAppearances
      friends {
        ...NameAndAppearances
      }
    }
  }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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"
              ]
            }
          ]
        }
      ]
    }
  }
}

Этот запрос - валиден. Давайте взгянем на невалидные запросы.

Фрагмент не может ссылаться на самого себя или создавать цикл, так как это может вызвать бесконечный ответ! Ниже приведен тот же запрос, но без явных трех уровней вложенности:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  hero {
    ...NameAndAppearancesAndFriends
  }
}

fragment NameAndAppearancesAndFriends on Character {
  name
  appearsIn
  friends {
    ...NameAndAppearancesAndFriends
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "errors": [
    {
      "message": "Cannot spread fragment \"NameAndAppearancesAndFriends\" within itself.",
      "locations": [
        {
          "line": 11,
          "column": 5
        }
      ]
    }
  ]
}

Когда мы запрашиваем поля, мы должны запросить поле данного типа. Поэтому т. к. hero позвращает Character, мы должны запросить поле типа Character. Этот тип не имеет поле favoriteSpaceship, так что данный запрос неверен:

1
2
3
4
5
6
# INVALID: favoriteSpaceship does not exist on Character
{
  hero {
    favoriteSpaceship
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "errors": [
    {
      "message": "Cannot query field \"favoriteSpaceship\" on type \"Character\".",
      "locations": [
        {
          "line": 4,
          "column": 5
        }
      ]
    }
  ]
}

Каждый раз, когда мы запрашиваем поле и он возвращает что-то, отличное от scalar или enum, мы должны уточнить, какие данные мы хотим получить обратно из поля. Hero возвращает Character, и мы запрашивали поля name и appearsIn в нем; если мы это упустим, запрос не будет валидным:

1
2
3
4
# INVALID: hero is not a scalar, so fields are needed
{
  hero
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "errors": [
    {
      "message": "Field \"hero\" of type \"Character\" must have a selection of subfields. Did you mean \"hero { ... }    \"?",
      "locations": [
        {
          "line": 3,
          "column": 3
        }
      ]
    }
  ]
}

Аналогично, если поле имеет тип scalar, не имеет смысла запрашивать дополнительные поля в нем, это вызовет ошибку:

1
2
3
4
5
6
7
8
# INVALID: name is a scalar, so fields are not permitted
{
  hero {
    name {
      firstCharacterOfName
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "errors": [
    {
      "message": "Field \"name\" must not have a selection since type \"String!\" has no subfields.",
      "locations": [
        {
          "line": 4,
          "column": 10
        }
      ]
    }
  ]
}

Ранее, было отмечено, что запрос может запрашивать поля того же типа, что и в запросе; когда мы запрашиваем hero, который возвращает Character, мы можем запрашивать только поля, которые существуют в Character. Что произойдет, если запросим основную функцию R2-D2?

1
2
3
4
5
6
7
# INVALID: primaryFunction does not exist on Character
{
  hero {
    name
    primaryFunction
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "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 если Character - Droid, и игнорировать это поле в ином случае. Мы можем использовать фрагменты, о которых мы говорили ранее. Используя фрагмент от Droid и включив его, мы страхуемся, что можем запросить primaryFunction, только если он определен.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  hero {
    name
    ...DroidFields
  }
}

fragment DroidFields on Droid {
  primaryFunction
}
1
2
3
4
5
6
7
8
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}

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

1
2
3
4
5
6
7
8
{
  hero {
    name
    ... on Droid {
      primaryFunction
    }
  }
}
1
2
3
4
5
6
7
8
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}

Мы только затронули систему валидации; есть множество правил проверки, направленных на то, чтобы убедиться, что запрос GraphQL семантически полноценный. Спецификация немного углубляется в секцию "Validation", а директория validation в GraphQL.js содержит код, осуществляющий валидацию, совместимый со спецификацией GraphQL.

Комментарии