В этом руководстве показано, как создать GraphQL API для Trello-подобного приложения с использованием Node.js и Spikenail framework. Мы научимся создавать модели, определять отношения, настраивать контроль доступа и проверки.

Полный исходный код, который мы собираемся создать в этом руководстве, доступен здесь: https://github.com/spikenail/spikenail-example-cards

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

Дизайн приложения

В нашем приложении будут доски, списки и карточки, как в Trello. На каждой доске есть много списков. Каждый список содержит множество карточек.

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

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

Краткое введение в GraphQL

GraphQL - это язык запросов, созданный Facebook. Он предоставляет совершенно новый способ использования и создания API. По сути, GraphQL - это замена REST.

Одно из самых больших преимуществ GraphQL - это возможность запрашивать именно те ресурсы, которые нам нужны, и заменять несколько вызовов API одним запросом.

Взгляните на пример запроса. Мы запрашиваем только определенные поля и связанную модель в одном запросе:

{
    getBook(id: 123) {
        title
        author {
          id
          name
        }
    }
}

Пример ответа:

{
    "data": {
        "getBook": {
            "title": "The Great Gatsby",
            "author" {
                "id": 192501,
                "name": "F. Scott Fitzgerald"
            }
        }
    }
}

Узнать больше о GraphQL можно на официальном сайте.

Каркас с шипами

Spikenail - это фреймворк Node.js ES7 с открытым исходным кодом, который позволяет создавать GraphQL API с минимальным написанием кода или вообще без него.

Основная идея Spikenail заключается в том, чтобы создать сложный API, просто настроив его. Эта конфигурация может включать отношения, проверки, контроль доступа и все, что нам нужно.

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

Подробнее о Spikenail здесь: https://github.com/spikenail/spikenail

Предпосылки

У вас установлен node.js 7.6+

У вас есть база данных MongoDB

Установка

Установите генератор йомена для Spikenail

npm install -g generator-spikenail

Создайте каталог для своего проекта и запустите в нем генератор yeoman.

mkdir spikenail-example-cards
cd spikenail-example-cards
yo spikenail

Он формирует базовую структуру каталогов и устанавливает все необходимые зависимости.

Настроить источник данных

В настоящее время поддерживается только MongoDB. Задайте SPIKENAIL_EXAMPLE_CARDS_MONGO_CONNECTION_STRING переменную среды или отредактируйте config/sources.js файл.

Создавайте модели

Начнем с создания базовой модели платы.

Вы можете использовать генератор моделей, чтобы упростить создание модели:

yo spikenail:model board

Будет создан models/Board.js файл только с полем id

Давайте отредактируем его и добавим несколько свойств, чтобы наш Board.js стал:

Все форумы, которые создает пользователь, по умолчанию являются частными. memberships - это массив, в котором будет храниться информация о совместном использовании в формате:

[{
    userId: 123
    role: 'observer'
}, {
    userId: 456,
    role: 'member'
}]

memberships массив не должен редактироваться напрямую. Поэтому мы используем на нем свойство readOnly. В результате он вообще не будет отображаться в параметрах мутации обновления. Совместное использование доступа имеет более сложную логику, чем просто изменение поля в базе данных - обычно оно включает в себя отправку электронных писем, возможность приглашать незарегистрированных пользователей и т. Д., Мы не будем реализовывать это в нашем демонстрационном приложении.

Таким же образом создаем models/List.js модель:

и models/Card.js модель:

Давайте добавим модель пользователя, models/User.js

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

[{
    token: "user-random-token"
}, {
    token: "user-random-token-2"
}]

Создавайте отношения

Доска имеет много списков, принадлежащих ей. Давайте определим отношение «имеет много», добавив следующее свойство в свойства модели models/Board.js:

lists: {
  relation: 'hasMany',
  ref: 'list',
  foreignKey: 'boardId'
}

Приведенное выше определение можно упростить:

lists: {
  relation: 'hasMany'
}

В этом случае Spikenail попытается угадать точные параметры отношения. Таким образом, список models/Board.js становится:

Давайте аналогичным образом добавим отношения к моделям List и Card.

models/List.js

models/Card.js

Настройка контроля доступа

Добавим правила доступа для Board.js модели.

Правила помещены в массив acl схемы модели.

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

{
    allow: false,
    roles: '*',
    actions: '*'
}

Поля roles и actions можно не указывать, поэтому правило становится следующим:

{
    allow: false
}

Затем разрешите всем авторизованным пользователям создавать доски. Анонимные пользователи не могут создавать доски:

{
  allow: true,
  actions: 'create',
  roles: 'user'
}

Разрешить владельцу доски делать все:

{
  allow: true,
  roles: 'owner',
  actions: '*', 
}

Разрешить всем (в том числе анонимным ролям) читать содержимое форума, если оно помечено как не закрытое.

{
  allow: true,
  roles: '*',
  actions: 'read',  
  scope: { isPrivate: false }
}

Обратите внимание на свойство scope - правило ACL будет применяться только к тем документам, которые соответствуют определенному им условию MongoDB.

Разрешить читать любую доску (как частную, так и общедоступную) для пользователя, добавленного на доску в качестве наблюдателя или члена:

{
  allow: true,
  roles: ['observer', 'member'],
  actions: 'read'
}

Наконец, наша модель с правилами ACL становится:

Но как именно должны работать роли observer, member и owner? Мы не добавили никакой логики, которая позволяет Spikenail выбирать правильное поведение для их обработки.

В Spikenail есть несколько предопределенных статических ролей, например user - любое аутентифицированное лицо, anonymous - соответственно не аутентифицированное лицо.

А есть еще и динамические роли. Для роли owner Spikenail по умолчанию имеет следующую логику:

currentUser.id == item.userId

Но для observer и member мы должны определить нашу собственную логику. Это можно сделать, указав roles объект схемы модели. Модель Board.js стала:

Как видите, функция cond должна возвращать либо логическое значение, либо условие MongoDB.

Давайте добавим ACL для List.js

Обратите внимание, что любое существующее отношение не влияет на какое-либо правило ACL само по себе. Отношения влияют только на способ получения данных.

Запретить все для всех:

{
  allow: false
}

Разрешить всем, кто может обновлять доску, выполнять любые действия со списками:

{
  allow: true,
  actions: '*',
  roles: '*',
  checkRelation: {
    name: 'board',
    action: 'update'
  }
}

Обратите внимание на свойство checkRelation - оно позволяет нам определять контроль доступа на основе другой модели.

Если мы не хотим разрешать членам правления редактировать доску, например переименуйте его, мы должны дополнительно разрешить им изменять списки, так как приведенные выше правила могут ограничивать его:

{
  allow: true,
  actions: '*',
  roles: '*',
  checkRelation: {
    name: 'board',
    roles: ['member'],
    action: 'read'
  }
}

Модель становится:

Наконец, давайте определим ACL для модели Card.

Запретить все для всех:

{
  allow: false
}

Тот, кто может обновлять список, должен иметь возможность выполнять любые действия с картой:

{
  allow: true,
  actions: '*',
  roles: '*',
  checkRelation: {
    name: 'list',
    action: 'update'
  }
}

Тот, кто умеет читать список, должен уметь читать карточку:

{
  allow: true,
  actions: 'read',
  roles: '*',
  checkRelation: {
    name: 'list',
    action: 'read'
  }
}

Окончательный Card.js список:

Добавление проверок

Обычно данные, которые мы получаем от пользователей, нуждаются в проверке. Это легко сделать с помощью Spikenail.

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

Это можно было сделать следующим образом:

validations: [{
  field: 'name',
  assert: 'required'
}, {
  field: 'name',
  assert: 'maxLength',
  max: 50
}]

Окончательный Board.js список:

Запустите API

Войдите в корневой каталог вашего проекта и запустите npm start, чтобы запустить сервер.

Перейдите к http: // localhost: 5000 / graphql, чтобы открыть в браузере IDE GraphQL.

Если вы получаете код из репозитория spikenail-example-cards, вы можете установить SPIKENAIL_EXAMPLE_CARDS_MONGO_CONNECTION_STRING_TEST переменную среды и запустить npm test, который очистит указанную базу данных и заполнит ее тестовыми данными.

Для того, чтобы войти как какой-то пользователь - измените URL, добавив auth_token http: // localhost: 5000 / graphql? Auth_token = igor-secret-token

Давай зададим несколько вопросов.

Создайте доску:

mutation {
  createBoard(input: { name: "My private board", isPrivate: true }){
    board {
      id
      isPrivate
      name
    }
    errors {
      code
      field
      message
    }
  }
}

Ответ:

{
  "data": {
    "createBoard": {
      "board": {
        "id": "Ym9hcmQ6NTk2Zjg0M2QxOGE0ZjNlY2VlZTNiN2My",
        "isPrivate": true,
        "name": "My private board"
      },
      "errors": null
    }
  }
}

Запрос всех списков с карточками и информацией о досках.

query {
  viewer {
    allLists {
      edges {
        node {
          id
          name
          cards {
            edges {
              node {
                title
              }
            }
          }
          board {
            id
            name
          }
        }
      }
    }
  }
}

Пример ответа:

{
  "data": {
    "viewer": {
      "allLists": {
        "edges": [
          {
            "node": {
              "id": "bGlzdDo1OTQ2NmU4Y2M4NzJmMDhjMDgxNTk4ZDM=",
              "name": "My List",
              "cards": {
                "edges": [
                  {
                    "node": {
                      "title": "Card one"
                    }
                  },
                  {
                    "node": {
                      "title": "Card two"
                    }
                  },
                  {
                    "node": {
                      "title": "Card three"
                    }
                  }
                ]
              },
              "board": {
                "id": "Ym9hcmQ6NTkyYmZjOTA2ZjM5Zjc5MGNmNGI5Yjg3",
                "name": "My Board"
              }
            }
          },
          {
            "node": {
              "id": "bGlzdDo1OTQ2NmU4Y2M4NzJmMDhjMDgxNTk4ZDU=",
              "name": "My second List",
              "cards": {
                "edges": []
              },
              "board": {
                "id": "Ym9hcmQ6NTkyYmZjOTA2ZjM5Zjc5MGNmNGI5Yjg3",
                "name": "My Board"
              }
            }
          }
        ]
      }
    }
  }
}

Даже если указано all, если определенные элементы не видны правилами ACL, они будут исключены из ответа.

Следующий запрос вернет только те доски, имя которых начинается с Public:

query {
  viewer {
    allBoards(filter: { where: { name: { regexp: "^Public" } }, order: "id DESC" }) {
      edges {
        node {
          id
          userId
          name
        }
      }
    }
  }
}

Подробнее о возможных запросах читайте в документации Spikenail.

Заключение

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

Еще предстоит проделать большую работу с Spikenail, прежде чем он будет готов к производству.

Если вам нравится идея и концепция, то отметьте репозиторий на GitHub. Любая обратная связь будет принята с благодарностью.