Схемы и распознаватели GraphQL

В этой статье вы научитесь создавать собственные структуры данных в GraphQL API.

Примечание: Все, что мы рассмотрим, можно подробнее изучить в официальной документации GraphQL.

Требования

В этом руководстве мы настроим базовый API, потому особой подготовки эта работа не требует, однако навык отправки запросов для проверки данных лишним не будет.

Начальная настройка

Для простоты мы рекомендуем использовать Prepros или babel – они открывают некоторые из последних функций JavaScript, такие как импорт.

Читайте также: Модули ES6: импорт и экспорт в JavaScript

Важно! Если вы используете Prepros, убедитесь, что для вашего файла server.js включены параметры Babel и Auto Compile.

Для настройки сервера мы будем использовать graphql-yoga и nodemon. nodemon позволяет автоматически перезагружать сервер при изменении файла, а graphql-yoga дает простой инструментарий для настройки сервера GraphQL на основе Express. Установите эти пакеты.

$ npm i graphql-yoga nodemon

Читайте также: Автоматический перезапуск приложений Node.js с помощью nodemon

Настройка nodemon

Начнем с nodemon. Добавьте в раздел scripts файла проекта package.json строку:

"start": "nodemon server.js"

Тогда вместо команды node server сервер будет запускать npm run start.

{
  "name": "graphql-api",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "dependencies": {
    "graphql-yoga": "^1.16.7"
  },
  "devDependencies": {
    "nodemon": "^1.19.1"
  },
  "scripts": {
    "start": "nodemon server.js"
  },
  "author": "",
  "license": "ISC"
}

Настройка сервера

Теперь нужно открыть главный файл server.js и импортировать GraphQLServer из graphql-yoga, чтобы API работал на localhost:4000. Серверу понадобятся две вещи: определения типов, которые будут инициализировать и структурировать данные, и распознаватели, которые сообщают серверу, как получать и фильтровать данные. Чтобы организовать все правильно, давайте создадим переменные для определений и преобразователей и передадим их функции.

import { GraphQLServer } from 'graphql-yoga'

const typeDefs = ``; // Our definitions need to be in template literals

const resolvers = {};

const server = new GraphQLServer({ typeDefs, resolvers }) ;

server.start(() => console.log('server running'));

На данный момент эта конфигурация вернет ошибку, ведь мы еще не настроили схему в typeDefs.

Определение типов

Чтобы настроить базовый запрос, просто добавьте type Query {} со списком данных, которые хотите сделать доступными, и укажите типы возвращаемых данных. В GraphQL можно создавать пользовательские типы, но есть и несколько встроенных типов: String, Int (все действительные числа), Float (десятичные числа), Boolean, ID. Любой из вышеперечисленных типов, помещенный в скобки, представляет собой массив этого типа. Любой тип, заканчивающийся восклицательным знаком, не допускает значения NULL, – следовательно, он вернет ошибку, если ничего не отправить обратно.

Вот пример нескольких возможных вариантов. Обратите внимание: нескольких вариантов для одного и того же поля не может быть, мы делаем это просто для примера.

const typeDefs = `
  type Query {
    user: String      # null
    user: String!     # error 'Cannot return null for non-nullable field Query.user'
    users: [String!]! # must return an array of non-null strings
  }
`;

Теперь localhost:4000 больше не выдает сообщение об ошибке. Однако вы все еще получите ошибку или ноль при выполнении запроса, поскольку мы еще не сказали серверу, как получить данные.

Настройка распознавателей

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

const resolvers = {
  Query: {
    user(){
      return 'I am a string';
    },
    users(){
      const names = ['Chomp', 'Jaws', 'Alli'];
      return names;
    }
  }
};

Пользовательские типы

Такие типы, как String, Int и Boolean, поставляются с GraphQL “из коробки”. Как мы упоминали выше, в GraphQL также можно создавать пользовательские типы, которые будут возвращать гораздо более сложные данные, и передавать эти типы в запрос (чтобы получить возможность запрашивать эти данные).

В рамках typeDefs (но вне Query), мы можем создать наш собственный тип, например, User (лучше всего записывать имена пользовательских типов с заглавной буквы), и присвоить ему необходимые данные. Как и в Query, каждый элемент в нашем типе также должен быть типизирован (например, age: Int!). Мы получим данные, возвращенные соответствующим распознавателем.

const typeDefs = `
  type Query {
    user: User! # Must be of our custom type, User
  }

  type User {
    name: String!
    age: Int!
    friends: [String]!
  }
`;

В рамках распознавателя мы можем просто передать объект с данными.

const resolvers = {
  Query: {
    user(){
      return {
        name: 'Chomp',
        age: 47,
        friends: ['Alli', 'Jaws']
      };
    }
  }
};

Настройка данных

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

const users = [
  {
    name: 'Chomp',
    age: 47,
    friends: ['Alli', 'Jaws']
  },{
    name: 'Alli',
    age: 16,
    friends: ['Chomp', 'Jaws']
  },{
    name: 'Jaws',
    age: 35,
    friends: ['Alli', 'Chomp']
  },
];

Поскольку в дальнейшем мы собираемся извлечь весь массив users, мы сообщим typeDefs, что нам нужен массив типа User, и распознаватель просто вернет весь массив.

const typeDefs = `
 type Query {
    user: [User!]!
  }

  type User {
    name: String!
    age: Int!
    friends: [String]!
  }
`;

const resolvers = {
  Query: {
    user(){
      return users;
    }
  }
};

Аргументы

Сейчас у нас есть возможность вернуть только один конкретный фрагмент данных, то есть все данные. А что, если нам нужна лишь определенная их часть? Можем ли мы отфильтровать результат?

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

const typeDefs = `
 type Query {
    user(name: String!): [User!]!
  }
`;

У распознавателей есть доступ к некоторым очень полезным объектам, которые предоставляет GraphQl. На данный момент все, что нам нужно, это parent и args.

  • parent: информация о родительском элементе (если у вас есть вложенные пользовательские типы). Предположим, если бы у нас был тип User, который обращался бы к типу Post, то каждая публикация имела бы доступ к данным о пользователе.
  • args: все аргументы, переданные в запрос.
  • ctx (сокращение от context): что угодно, что может потребоваться в запросе, например данные аутентификации.
  • info: Информация о состоянии запроса.

Давайте попробуем проверить, есть ли вообще в args какой-либо аргумент, и, если он есть, использовать filter(), чтобы вернуть данные только тех пользователей, имя которых совпадает с нашим аргументом.

const resolvers = {
  Query: {
    user(parent, args, ctx, info){
      if (!args.name) return users;

      else return users.filter(user => {
        return user.name.toLowerCase().includes(args.name.toLowerCase());
      });
    }
  }
};

В localhost:4000 мы можем использовать аргументы так:

{
  user(name: "alli") {
    name
    friends
  }
}

Реляционные данные

Чтобы использовать больше вложенных данных, мы можем добавить свои пользовательские типы в другие пользовательские типы. При этом нам нужно добавить еще один преобразователь вне Query для этого типа данных.

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

Распознаватель создаст пустой объект, затем переберет всех друзей в parent, поместит каждого подходящего пользователя в массив friends и вернет список друзей в запрос.

const typeDefs = `
 type Query {
    user(name: String!): [User!]!
  }

  type User {
    name: String!
    age: Int!
    friends: [User!]!
  }
`;

const resolvers {
  Query: { ... },
  User: { // The nested type that we're querying for
    friends(parent, args, ctx, info) {
        const friends = [];

        parent.friends.forEach(friend => users.filter(user => {
          if (user.name.toLowerCase().includes(friend.toLowerCase())) friends.push(user);
        }));

        return friends;
    }
  }
};

Заключение

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

Если у вас возникли проблемы с примерами, обратитесь к этому репозиторию.

Tags: ,

Добавить комментарий