Часть 4. Создание API REST: обработка запросов POST, PUT и DELETE

В предыдущей статье вы добавили логику в API для запросов GET, которые извлекали данные из базы данных. В этом посте вы завершите построение базовой функциональности API CRUD, добавив логику для обработки запросов POST, PUT и DELETE

Добавление логики маршрутизации

Чтобы упростить логику маршрутизации, вы будете перенаправлять все методы HTTP через существующий маршрут (с необязательным параметром id). Откройте файл services / router.js и замените текущую логику маршрутизации (строки 5-6) следующим кодом:

router.route('/employees/:id?')
  .get(employees.get)
  .post(employees.post)
  .put(employees.put)
  .delete(employees.delete);

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

Обработка POST-запросов

HTTP POST-запросы используются для создания новых ресурсов (в данном случае это записи сотрудников). Основная идея состоит в том, чтобы извлечь данные из тела HTTP-запроса и использовать его для создания новой строки в базе данных. Чтобы добавить логику контроллера для запросов POST, откройте файл controllers / employee.js и добавьте следующий код:

function getEmployeeFromRec(req) {
  const employee = {
    first_name: req.body.first_name,
    last_name: req.body.last_name,
    email: req.body.email,
    phone_number: req.body.phone_number,
    hire_date: req.body.hire_date,
    job_id: req.body.job_id,
    salary: req.body.salary,
    commission_pct: req.body.commission_pct,
    manager_id: req.body.manager_id,
    department_id: req.body.department_id
  };
 
  return employee;
}
 
async function post(req, res, next) {
  try {
    let employee = getEmployeeFromRec(req);
 
    employee = await employees.create(employee);
 
    res.status(201).json(employee);
  } catch (err) {
    next(err);
  }
}
 
module.exports.post = post;

Функция getEmployeeFromRec принимает объект запроса и возвращает объект со свойствами, необходимыми для создания записи сотрудника. Функция была объявлена вне функции post, чтобы ее можно было использовать позже и для запросов PUT.

Функция post использует getEmployeeFromRec для инициализации переменной, которая затем передается методу create API базы данных сотрудников. После операции создания клиенту отправляется код состояния «201 Создан» вместе с JSON сотрудника (включая новое значение идентификатора сотрудника).

Теперь вы можете обратите внимание на логику в API базы данных. Откройте файл db_apis / employee.js и добавьте следующий код внизу.

const createSql =
 `insert into employees (
    first_name,
    last_name,
    email,
    phone_number,
    hire_date,
    job_id,
    salary,
    commission_pct,
    manager_id,
    department_id
  ) values (
    :first_name,
    :last_name,
    :email,
    :phone_number,
    :hire_date,
    :job_id,
    :salary,
    :commission_pct,
    :manager_id,
    :department_id
  ) returning employee_id
  into :employee_id`;
 
async function create(emp) {
  const employee = Object.assign({}, emp);
 
  employee.employee_id = {
    dir: oracledb.BIND_OUT,
    type: oracledb.NUMBER
  }
 
  const result = await database.simpleExecute(createSql, employee);
 
  employee.employee_id = result.outBinds.employee_id[0];
 
  return employee;
}
 
module.exports.create = create;

Приведенная выше логика начинается с объявления константы с именем createSql для хранения оператора вставки. Обратите внимание, что он использует переменные связывания, а не конкатенацию строк, для ссылки на значения, которые нужно вставить. Стоит повторить, насколько важны bind variables по соображениям безопасности и производительности. Старайтесь избегать конкатенации строк, когда это возможно.

Внутри функции create постоянная сотрудника определяется и инициализируется для копии параметра emp с помощью Object.assign. Это предотвращает прямую модификацию объекта, переданного из контроллера.

Затем свойство employee_id добавляется к объекту employee (настроенному как «out bind»), чтобы оно содержало все переменные связывания, необходимые для выполнения оператора SQL. Затем функция simpleExecute используется для выполнения оператора вставки, а свойство outBinds для перезаписи свойства employee.employee_id перед возвратом объекта.

Поскольку на модуль oracledb есть ссылка, вам потребуется добавить следующую строку в начало файла.
const oracledb = require('oracledb');


Обработка запросов PUT

Запросы PUT будут использоваться для обновления существующих ресурсов. Важно отметить, что PUT используется для замены всего ресурса — он не выполняет частичные обновления (я покажу вам, как сделать это с PATCH в будущем). Вернитесь в файл controllers / employee.js и добавьте следующий код внизу.

async function put(req, res, next) {
  try {
    let employee = getEmployeeFromRec(req);
 
    employee.employee_id = parseInt(req.params.id, 10);
 
    employee = await employees.update(employee);
 
    if (employee !== null) {
      res.status(200).json(employee);
    } else {
      res.status(404).end();
    }
  } catch (err) {
    next(err);
  }
}
 
module.exports.put = put;

Функция put использует getEmployeeFromRec для инициализации объекта с именем employee, а затем добавляет свойство employee_id из параметра id в URL. Затем объект employee передается в функцию обновления API базы данных, и клиенту на основании результата отправляется соответствующий ответ.

Чтобы добавить логику обновления в API базы данных, добавьте следующий код в файл db_apis / employee.js.

const updateSql =
 `update employees
  set first_name = :first_name,
    last_name = :last_name,
    email = :email,
    phone_number = :phone_number,
    hire_date = :hire_date,
    job_id = :job_id,
    salary = :salary,
    commission_pct = :commission_pct,
    manager_id = :manager_id,
    department_id = :department_id
  where employee_id = :employee_id`;
 
async function update(emp) {
  const employee = Object.assign({}, emp);
  const result = await database.simpleExecute(updateSql, employee);
 
  if (result.rowsAffected && result.rowsAffected === 1) {
    return employee;
  } else {
    return null;
  }
}
 
module.exports.update = update;

Логика обновления очень похожа на логику создания. Объявлена переменная для хранения оператора SQL, а затем simpleExecute используется для выполнения оператора с переданными динамическими значениями (после копирования их в другой объект). result.rowsAffected используется, чтобы определить, было ли обновление успешным, и вернуть правильное значение.

Обработка запросов DELETE

Последний метод, который вам нужно реализовать, это DELETE, который, что неудивительно, удалит ресурсы из базы данных. Добавьте следующий код в конец файла controllers / employee.js.

async function del(req, res, next) {
  try {
    const id = parseInt(req.params.id, 10);
 
    const success = await employees.delete(id);
 
    if (success) {
      res.status(204).end();
    } else {
      res.status(404).end();
    }
  } catch (err) {
    next(err);
  }
}
 
module.exports.delete = del;

Движок JavaScript выдаст исключение, если вы попытаетесь объявить функцию с именем «delete», используя оператор функции. Чтобы обойти это, объявляется функция с именем «del», а затем экспортируется как «delete».

На этом этапе вы должны уметь читать и понимать логику. В отличие от предыдущих примеров, этот HTTP-запрос не имеет тела, используется только параметр id в пути маршрута. Код состояния «204 Нет содержимого» часто используется с запросами DELETE, когда тело ответа не отправляется.

Чтобы завершить логику базы данных, вернитесь в файл db_apis / employee.js и добавьте следующий код в конец.

const deleteSql =
 `begin
 
    delete from job_history
    where employee_id = :employee_id;
 
    delete from employees
    where employee_id = :employee_id;
 
    :rowcount := sql%rowcount;
 
  end;`
 
async function del(id) {
  const binds = {
    employee_id: id,
    rowcount: {
      dir: oracledb.BIND_OUT,
      type: oracledb.NUMBER
    }
  }
  const result = await database.simpleExecute(deleteSql, binds);
 
  return result.outBinds.rowcount === 1;
}
 
module.exports.delete = del;

Поскольку таблица JOB_HISTORY имеет ограничение внешнего ключа, которое ссылается на таблицу EMPLOYEES, простой блок PL / SQL используется для удаления необходимых строк из обеих таблиц за один цикл.

Разбор JSON-запросов

Если вы посмотрите на функцию getEmployeeFromRec в controllers / employee.js, вы заметите, что свойство body запроса — это объект JavaScript. Это обеспечивает простой способ получения значений из тела запроса, но это не происходит автоматически.

API, который вы создаете, ожидает, что клиенты будут отправлять данные в формате JSON в теле запросов POST и PUT. Кроме того, клиенты должны установить заголовок Content-Type запроса на application / json, чтобы веб-сервер знал, какой тип тела запроса отправляется. Вы можете использовать встроенное промежуточное программное обеспечение express.json, чтобы Express мог анализировать такие запросы.

Откройте файл services / web-server.js и добавьте следующие строки чуть ниже вызова app.use, который добавляет morgan в конвейер запросов.

// Parse incoming JSON requests and revive JSON.
    app.use(express.json({
      reviver: reviveJson
    }));

Когда данные JSON распарсятся в native объекты JavaScript, тогда только типы данных, поддерживаемые в JSON, будут правильно сопоставлены с типами JavaScript. Даты не поддерживаются в JSON и обычно представлены в виде строк ISO 8601. Используя функцию reviver, переданную промежуточному программному обеспечению express.json, вы можете выполнить преобразования вручную. Добавьте следующий код в конец файла services / web-server.js.

const iso8601RegExp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
 
function reviveJson(key, value) {
  // revive ISO 8601 date strings to instances of Date
  if (typeof value === 'string' && iso8601RegExp.test(value)) {
    return new Date(value);
  } else {
    return value;
  }
}

Тестирование API

Пришло время протестировать новую функциональность CRUD! До сих пор вы использовали браузер для тестирования API, но это не будет работать для запросов POST, PUT и DELETE. Я покажу вам, как тестировать API с помощью команд curl, потому что он легко доступен в виртуальной машине. Но можно использовать и графический инструмент, такой как Postman, Insomina (бесплатно).

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

curl -X "POST" "http://localhost:3000/api/employees" \
     -i \
     -H 'Content-Type: application/json' \
     -d $'{
  "first_name": "Dan",
  "last_name": "McGhan",
  "email": "dan@fakemail.com",
  "job_id": "IT_PROG",
  "hire_date": "2014-12-14T00:00:00.000Z",
  "phone_number": "123-321-1234"
}'

Если запрос был успешным, ответ должен содержать объект employee с атрибутом employee_id. Вот пример:

image

В моем случае значение employee_id было 227 — вам нужно будет изменить следующие команды, основываясь на полученном значении employee_id.

Например, чтобы обновить новую запись, введите PUT для URL с этим значением идентификатора.

curl -X "PUT" "http://localhost:3000/api/employees/227" \
     -i \
     -H 'Content-Type: application/json' \
     -d $'{
  "first_name": "Dan",
  "last_name": "McGhan",
  "email": "dan@fakemail.com",
  "job_id": "AD_PRES",
  "hire_date": "2014-12-14T00:00:00.000Z",
  "phone_number": "123-321-1234"
}'

Триггер UPDATE_JOB_HISTORY в схеме HR обнаружит изменение задания и добавит строку в таблицу JOB_HISTORY. Если вы посмотрите в эту таблицу, вы должны увидеть запись для нового сотрудника. Выполните следующую команду, чтобы удалить историю заданий и записи сотрудников.

curl -i -X "DELETE" "http://localhost:3000/api/employees/227"


И вот, у вас есть все, полная функциональность CRUD!

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

Комментарии (0)