Использование бессерверных функций для поддержки конечных точек REST в 8base Workspace

По умолчанию платформа 8base автоматически генерирует чрезвычайно мощный API GraphQL, который дает вам немедленный доступ API к вашим данным. Однако иногда разработчики используют сторонние службы или другой инструмент, который не поддерживает создание и выполнение запросов GraphQL. Вместо этого они требуют, чтобы был доступен REST API (или отдельные конечные точки).

Разработать REST API в 8base легко, используя тип Пользовательская функция Webhook. Используя Webhooks, разработчик может быстро кодировать и развертывать бессерверные функции, которые становятся доступными с использованием традиционных HTTP-глаголов (GET, POST, PUT, DELETE и т. Д.) И уникального пути.

Приступая к работе над созданием REST API на 8base

Чтобы приступить к созданию REST API поверх рабочей области 8base, вам необходимо создать / установить следующие ресурсы.

  1. Рабочее пространство 8base (Бесплатный уровень или Платный план)
  2. Установлен 8base CLI (Инструкции доступны здесь)
  3. Текстовый редактор или IDE (VS Code, Atom, Sublime или любой другой способ написания кода)

Когда все это будет готово, откройте командную строку и используйте следующие команды для создания нового серверного проекта 8base.

# If not authenticated, login via the CLI
$ 8base login
# Generate a new 8base project and select your desired workspace
$ 8base init rest-api-tutorial
# Move into the new directory
$ cd rest-api-tutorial

Вот и все!

Создание бессерверных функций для вашего REST API

Давайте продолжим и сгенерируем все наши бессерверные функции. Мы можем сделать это довольно быстро, используя команды генератора 8base CLI. Используя генераторы, мы сможем создать каждую из наших функций для каждой конечной точки нашего REST API.

# Our Index records endpoint
8base generate webhook getUsers — method=GET — path=’/users’ — syntax=js
# Our create record endpoint
8base generate webhook newUser — method=POST — path=’/users’ — syntax=js
# Our get record endpoint
8base generate webhook getUser — method=GET — path=’/users/{id}’ — syntax=js
# Our edit record endpoint
8base generate webhook editUser — method=PUT — path=’/users/{id}’ — syntax=js
# Our delete record endpoint
8base generate webhook deleteUser — method=DELETE — path=’/users/{id}’ — syntax=js

Как вы, наверное, уже заметили, мы создаем REST API, который предоставляет доступ к таблице Users в нашей рабочей области 8base. Это связано с тем, что таблица Users по умолчанию создается вместе с рабочей областью, поэтому вам не нужно создавать какие-либо новые таблицы для учебника. Тем не менее, тот же шаблон, который мы рассмотрим, будет работать для любой другой таблицы базы данных, которую вы решите использовать (или нескольких).

Кроме того, поскольку мы просто создаем этот API для таблицы Users, ничего страшного, что все эти функции сгруппированы на верхнем уровне директора src/webhooks. Если вы создаете REST API, который будет обрабатывать намного больше таблиц или больше настраиваемых конечных точек, эта структура каталогов может быстро оказаться загруженной / неорганизованной.

Ничто не мешает вам реструктурировать каталог в соответствии с вашими организационными потребностями! Все, что вам нужно сделать, это убедиться, что в объявлении функции в файле 8base.yml указан действительный путь к файлу / скрипту обработчика функции. Например, взгляните на следующую структуру каталогов и 8base.yml файл:

Структура каталогов

Файл 8base.yml

functions:
  listUsers:
    type: webhook
    handler:
      code: src/webhooks/users/list/handler.js
    path: /users
    method: GET
  getUser:
    type: webhook
    handler:
      code: src/webhooks/users/get/handler.js
    path: '/users/{id}'
    method: GET
  createUser:
    type: webhook
    handler:
      code: src/webhooks/users/create/handler.js
    path: /users
    method: POST
  editUser:
    type: webhook
    handler:
      code: src/webhooks/users/edit/handler.js
    path: '/users/{id}'
    method: PUT
  deleteUser:
    type: webhook
    handler:
      code: src/webhooks/users/delete/handler.js
    path: '/users/{id}'
    method: DELETE

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

Написание наших бессерверных функций REST API

Теперь, когда у нас есть сгенерированные бессерверные функции, давайте продолжим и начнем добавлять к ним некоторый код. Что важно знать о функции Webhook, так это то, что два важных объекта передаются в функцию через аргумент события. Это data и pathParameters.

Аргумент данных используется для доступа к любым данным, отправленным через запрос POST или PUT. Между тем, любые параметры запроса или параметры URL-адреса, отправленные через запрос, становятся доступными в объекте pathParameters. Следовательно, если к конечной точке /users/{id}?local=en был сделан запрос GET, значения для id и local будут доступно через event.pathParameters[KEY].‍

ПОЛУЧИТЬ конечную точку пользователя

Зная это, давайте настроим конечную точку GET User (/users/{id})! Чтобы помочь с нашими запросами GraphQL внутри функции, добавьте пакет NPM GraphQL Tag с помощью npm install -s graphql-tag. Затем скопируйте приведенный ниже код в файл обработчика вашей getUser функции.

/* Bring in any required imports for our function */
import gql from "graphql-tag";
import { responseBuilder } from "../utils";
/* Declare the Query that gets used for the data fetching */
const QUERY = gql`
  query($id: ID!) {
    user(id: $id) {
      id
      firstName
      lastName
      email
      createdAt
      updatedAt
      avatar {
        downloadUrl
      }
      roles {
        items {
          name
        }
      }
    }
  }
`;
module.exports = async (event, ctx) => {
  /* Get the customer ID from Path Parameters */
  let { id } = event.pathParameters;
  let { user } = await ctx.api.gqlRequest(QUERY, { id });
  if (!user) {
    return responseBuilder(404, { message: `No record found.`, errors: [] });
  }
  return responseBuilder(200, { result: user });
};

Скорее всего, вы заметите нераспознанный импорт; responseBuilder. Webhook требует, чтобы в возвращаемых объектах были объявлены следующие ключи - statusCode, body и (необязательно) headers. Вместо того, чтобы явно записывать каждый объект ответа, мы можем начать их генерировать с помощью удобной responseBuilder функции.

Итак, давайте продолжим и создадим новый каталог и файл, используя следующие команды, а затем поместим туда нашу функцию responseBuilder.

$ mkdir src/webhooks/utils
$ touch src/webhooks/utils/index.js

Скопируйте следующий скрипт.

/**
 * Webhook response objects require a statusCode attribute to be specified.
 * A response body can get specified as a stringified JSON object and any
 * custom headers set.
 */
export const responseBuilder = (code = 200, data = {}, headers = {}) => {
  /* If the status code is greater than 400, error! */
  if (code >= 400) {
    /* Build the error response */
    const err = {
      headers,
      statusCude: code,
      body: JSON.stringify({
        errors: data.errors,
        timestamp: new Date().toJSON(),
      }),
    }; /* Console out the detailed error message */
    console.log(err); /* Return the err */
    return err;
  }
  return {
    headers,
    statusCode: code,
    body: JSON.stringify(data),
  };
};

Потрясающие! Почти как если бы мы создавали метод контроллера, который запускает SQL-запрос и затем возвращает некоторые сериализованные данные, здесь мы использовали тот же шаблон, но с использованием бессерверной функции, которая использует GraphQL API.

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

Конечная точка GET Users

Теперь давайте настроим способ составления списка всех пользователей через наш REST API. Скопируйте приведенный ниже код в файл обработчика вашей getUsers функции.

import gql from 'graphql-tag'
import { responseBuilder } from '../utils'
const QUERY = gql`
  query {
    usersList {
      count
      items {
        id
        firstName
        lastName
        email
        createdAt
        updatedAt
      }
    }
  }
`
module.exports = async (event, ctx) => {
  /* Get the customer ID from Path Parameters */
  let { usersList } = await ctx.api.gqlRequest(QUERY)
return responseBuilder(200, { result: usersList })
}

POST Пользовательская конечная точка

Теперь давайте настроим способ добавления новых пользователей через наш REST API. Скопируйте приведенный ниже код в файл обработчика вашей newUser функции.

import gql from 'graphql-tag'
import { responseBuilder } from '../../utils'

const MUTATION = gql`
  mutation($data: UserCreateInput!) {
    userCreate(data: $data) {
      id
      email
      firstName
      lastName
      updatedAt
      createdAt
    }
  }
`
module.exports = async (event, ctx) => {
  /** 
   * Here we're pulling data out of the request to 
   * pass it as the mutation input
   */
  const { data } = event

  try {
    /* Run mutation with supplied data */
    const { userCreate } = await ctx.api.gqlRequest(MUTATION, { data })

    /* Success response */
    return responseBuilder(200, { result: userCreate })
  } catch ({ response: { errors } }) {

    /* Failure response */
    return responseBuilder(400, { errors })
  }
}

PUT Пользовательская конечная точка

Теперь давайте настроим способ редактирования пользователей через наш REST API. Скопируйте приведенный ниже код в файл обработчика вашей editUser функции.

import gql from 'graphql-tag'
import { responseBuilder } from '../../utils'
const MUTATION = gql`
  mutation($data: UserUpdateInput!) {
    userUpdate(data: $data) {
      id
      email
      firstName
      lastName
      updatedAt
      createdAt
    }
  }
`
module.exports = async (event, ctx) => {
  const { id } = event.pathParameters
/* Combine the pathParameters with the event data */
  const data = Object.assign(event.data, { id })
try {
    /* Run mutation with supplied data */
    const { userUpdate } = await ctx.api.gqlRequest(MUTATION, { data })
/* Success response */
    return responseBuilder(200, { result: userUpdate })
  } catch ({ response: { errors } }) {
    /* Failure response */
    return responseBuilder(400, { errors })
  }
}

УДАЛИТЬ конечную точку пользователя

Теперь давайте настроим способ редактирования пользователей через наш REST API. Скопируйте приведенный ниже код в файл обработчика вашей deleteUser функции.

import gql from 'graphql-tag'
import { responseBuilder } from '../../utils'
const MUTATION = gql`
  mutation($id: ID!) {
    userDelete(data: { id: $id }) {
      success
    }
  }
`
module.exports = async (event, ctx) => {
  const { id } = event.pathParameters
try {
    /* Run mutation with supplied data */
    const { userDelete } = await ctx.api.gqlRequest(MUTATION, { id })
/* Success response */
    return responseBuilder(200, { result: userDelete })
  } catch ({ response: { errors } }) {
    /* Failure response */
    return responseBuilder(400, { errors })
  }
}

Тестирование нашего REST API локально

Отличная работа! Довольно просто, правда? Что дальше - чрезвычайно важный шаг; тестирование. То есть, как нам запустить эти функции локально, чтобы убедиться, что они ведут себя так, как ожидалось?

Возможно, вы заметили каталог под названием mocks, который находится в каждом из каталогов функции. По сути, макеты позволяют нам структурировать полезную нагрузку JSON, которая передается в качестве аргумента события нашей функции при локальном тестировании. Объект JSON, объявленный в фиктивном файле, будет тем же аргументом, который передается функции при тестировании - ни больше, ни меньше.

Тем не менее, давайте продолжим и запустим нашу функцию getUsers, поскольку она игнорирует аргумент события. Мы можем сделать это с помощью команды invoke-local CLI, а также ожидать ответа, который будет выглядеть следующим образом:

$ 8base invoke-local listUsers
=> Result:
{
 “headers”: {},
 “statusCode”: 200,
 “body”: “{\”result\”:{\”count\”:1,\”items\”:[{\”id\”:\”SOME_USER_ID\”,\”firstName\”:\”Fred\”,\”lastName\”:\”Scholl\”,\”email\”:\”[email protected]\”,\”createdAt\”:\”2020–11–19T19:26:53.922Z\”,\”updatedAt\”:\”2020–11–19T19:46:59.775Z\”}]}}”
}

Скопируйте идентификатор первого возвращенного пользователя в ответ. Мы собираемся использовать его для создания макета для нашей функции getUser. Итак, теперь добавьте следующий JSON в файл src/webhooks/getUser/mocks/request.json.

{
 “pathParameters”: {
 “id”: “[SOME_USER_ID]”
 }
}

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

$ 8base invoke-local getUser -m request
=> Result:
{
 “headers”: {},
 “statusCode”: 200,
 “body”: “{\”result\”:{\”id\”:\”SOME_USER_ID\”,\”firstName\”:\”Fred\”,\”lastName\”:\”Scholl\”,\”email\”:\”[email protected]\”,\”createdAt\”:\”2020–11–19T19:26:53.922Z\”,\”updatedAt\”:\”2020–11–19T19:46:59.775Z\”,\”avatar\”:null,\”roles\”:{\”items\”:[]}}}”
}

А что, если вы хотите указать данные? Мол, когда хотите протестировать обновление? Применяется принцип точной выборки. Мы добавляем ключ данных в наш макет с данными, которые мы ожидаем отправить в нашу конечную точку. Попробуйте сами, добавив следующий JSON в src/webhooks/editUser/mocks/request.json файл.

{
 “data”: {
  “firstName”: “Freddy”,
  “lastName”: “Scholl”,
  “email”: “[email protected]”
 },
 “pathParameters”: {
  “id”: “SOME_USER_ID”
 }
}

Наконец, не все запросы к API всегда успешны… Из-за этого мы добавили обработку ошибок в наши функции! Кроме того, было бы настоящей проблемой постоянно редактировать ваш фиктивный файл, чтобы сначала проверить успех, затем неудачу и т. Д.

Чтобы помочь с этим, вы можете создать столько разных файлов-макетов, сколько захотите, и ссылаться на них по имени! Генератор CLI поможет вам и поместит макет в соответствующий каталог. Например:

# Mock for a valid input for the editUser function
8base generate mock editUser — mockName success
# Mock for a invalid input for the editUser function
8base generate mock editUser — mockName failure

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

# Test an unsuccessful response
8base invoke-local editUser -m failure
=> Result:
{
 headers: {},
 statusCode: 400,
 body: “{\”errors\”: [\r\n {\r\n \”message\”: \”Record for current filter not found.\”,\r\n \”locations\”: [],\r\n \”path\”: [\r\n \”userUpdate\”\r\n ],\r\n \”code\”: \”EntityNotFoundError\”,\r\n \”details\”: {\r\n \”id\”: \”Record for current filter not found.\”\r\n }\r\n }\r\n ],\r\n \”timestamp\”: \”2020–11–20T01:33:38.468Z\”\r\n}”
}

Развертывание нашего REST API на 8base

Развертывание здесь будет самой простой частью. Беги 8base deploy… вот и все.

Однако в этот момент вы можете задать себе животрепещущий вопрос: «Где мне найти свои конечные точки?» Когда все будет готово к развертыванию, запустите 8base describe в интерфейсе командной строки. У вас должно получиться что-то вроде этого:

Все ваши конечные точки теперь доступны по адресу https://api.8base.com/{PATH_IN_TABLE_ABOVE}.

заворачивать

8base - это простой в использовании и масштабируемый бэкэнд приложения с автоматически генерируемым GraphQL API. Тем не менее, для тех разработчиков, которые создают приложения, требующие интерфейсов REST API, я надеюсь, что это руководство дало вам несколько полезных подсказок о том, как много можно сделать с помощью 8base!

Не стесняйтесь обращаться с любыми вопросами!