express03.md 12 KB

Использование sequelize для получения данных из БД

Продолжаем разработку АПИ для проекта "ресторан" используя express.js.

Содержание:

Возвращаемся к нашему app.js

Добавляем в начале файла импорт библиотеки

const { sequelize } = require('./models')
const { QueryTypes } = require('sequelize')

И добавим endpoint для получения списка блюд (я добавил префикс /api, он нам понадобится при настройке nginx, когда будем заворачивать апи в контейнер):

app.get('/api/menu-item', async (req, res) => {
  try {
    res.json(await sequelize.query(`
      SELECT *
      FROM MenuItem
    `, {
      logging: false,
      type: QueryTypes.SELECT
    }))
  } catch (error) {
    console.error(error)
  } finally {
    res.end()
  }
})
  1. Запросы к БД асинхронные, поэтому заворачиваем код в async/await
  2. Список блюд, возвращаемый запросом sequelize.query заворачиваем в res.json(), это добавит в заголовок ответа Content-Type: application/json.

Пока у нас таблица блюд пустая, поэтому в ответе тоже будет пустой массив

Создание скрипта для наполнения БД начальными данными

Sequelize позволяет добавить записи в таблицы (можно использовать для первоначальной инициализации словарей или при тестировании)

npx sequelize-cli seed:generate --name menu-items

В каталоге seeders будет создан файл аналогичный файлам миграции (собственно и отличий между ними нет, просто логически выделены отдельно)

С помощью метода bulkInsert формируем список блюд для добавления:

'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up (queryInterface, Sequelize) {
    await queryInterface.bulkInsert('MenuItem', [   
      {id: 1, title: 'Салат', image: 'Салат.jpg', price: 100},
      {id: 2, title: 'Суп', image: 'Суп.jpg', price: 100},
      {id: 3, title: 'Компот', image: 'Компот.jpg', price: 100}
    ])
  },

  async down (queryInterface, Sequelize) {
    await queryInterface.bulkDelete(
      'MenuItem', 
      null /* тут можно прописать условие WHERE */
    )
  }
}

И запускаем добавление данных командой

npx sequelize-cli db:seed:all [--url=...]

Можно откатить отдельный импорт, но накатить можно только всё разом. Поэтому для заполнения словарей лучше использовать bulkInsert в обычной миграции, а seeders использовать для тестов.

Добавление сущностей БД, реализация REST.

Добавим в БД таблицу Cart (корзина) и связи между Cart и MenuItem

npx sequelize-cli migration:generate --name cart

И пропишем в созданном файле команды для создания таблицы с внешним ключем и команды для удаления:

В реальном проекте ещё нужно добавить таблицу пользователей и привязать корзину к пользователю

'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up (queryInterface, Sequelize) {
    await queryInterface.createTable('Cart', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.DataTypes.INTEGER
      },
      menuItemId: {
        type: Sequelize.DataTypes.INTEGER,
        allowNull: false,
        comment: 'для внешнего ключа'
      },
      quantity: {
        type: Sequelize.DataTypes.INTEGER,
        allowNull: false,
        comment: 'количество'
      }
    })

    /**
     * Внешний ключ корзина_блюдо
     */
    await queryInterface.addConstraint('Cart', {
      fields: ['menuItemId'],
      type: 'foreign key',
      name: 'FK_cart_menu-item',
      references: {
          table: 'MenuItem',
          field: 'id'
      },
      onDelete: 'no action',
      onUpdate: 'no action'
    })

  },

  async down (queryInterface, Sequelize) {
    await queryInterface.removeConstraint('Cart', 'FK_cart_menu-item')
    await queryInterface.dropTable('Cart')
  }
}

Накатываем миграцию командой npx sequelize-cli db:migrate и у нас в схеме бд появится таблица Cart:

Роутер

Мы уже знаем как создавать конечные точки, и можем даже добавить в них параметры пути, например, написать такую конечную точку, чтобы получить детальную информацию по блюду:

app.get('/api/menu-item/:id', (req, res) => {
  // req.params.id
})

Но базовый синтаксис express.js не позволяет задавать тип параметров передаваемых в пути запроса, мы не можем явно указать, что :id должен быть целым числом.

Для этих целей в express.js есть класс Router. Перепишем наше приложение, чтобы использовать роутеры:

  1. Создайте каталог routes в котором будем создавать отдельные файлы для сущностей

  2. Создайте файл routes/menu_item.js, добавьте в него обвязку для роутера и перенесите в него конечные точки для модели menu-item:

    const express = require('express')
        
    // создаем экземпляр роутера и экспортируем его из модуля
    const router = express.Router()
    module.exports = router
    
    // описываем конечные точки роутера
    // обратите внимание - начало пути (/api/menu-item) мы тут не пишем
    // тип параметра задаем используя regex-формат
    router.get('/:id(\\d+)', async (req, res) => {
      ...
    })
    
  3. В файле app.js подключаем созданные роутеры

    // импортируем маршруты
    const menuItemRouter = require('./routes/menu_item')
    
    const app = express()
    // цепляем все запросы начинающиеся с "/api/menu-item" с маршруту
    app.use('/api/menu-item', menuItemRouter)
    
    ...
    

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

CRUD для корзины

Для того, чтобы тело запроса автоматически преобразовывалось в объект body нужно после создания экземпляра приложения подключить middleware express.json() (middleware это функции, которые выполняются до обработки endpoints, используются для служебных целей, как в нашем случае, и в целях авторизации. Позже добавим jwt-авторизацию и напишем для неё middleware):

...
const app = express()
app.use(express.json())
...

Create. Для добавления записей в таблицу используется метод POST

Используется старый синтаксис без роутера, но вы пишите сразу правильно

app.post('/api/cart', async function(req, res) {
  try {
    await sequelize.query(`
      INSERT INTO Cart (menuItemId, quantity)
      -- значения :menuItemId и :quantity задаются в replacements
      VALUES (:menuItemId, :quantity)
    `,{
      logging: false,
      type: QueryTypes.INSERT,
      // объект replacements должен содержать значения для замены
      replacements: {
        menuItemId: req.body.menuItemId,
        quantity: req.body.quantity
      }
    })
    // ничего не возвращаем, но код ответа меняем на 201 (Created)
    // можно и не менять, по-умолчанию возвращает 200 (OK)
    res.status(201)
  } catch (error) {
    console.warn('ошибка при добавлении блюда в корзину:', error.message)
    // при ошибке возвращаем код ошибки 500 и текст ошибки
    res.status(500).send(error.message)
  } finally {
    res.end()
  }
})

Read. Чтение

Используется метод get. Пример приводить не буду, он аналогичен запросу списка блюд

Update. Редактирование корзины

Для редактирования в REST используются методы PUT или PATCH. PUT - полная перезапись (замещение объекта), PATCH - частичная перезапись (изменение части объекта)

Мы будем только менять количество, поэтому используем PATCH

// в пути можно описать параметр (поставив знак ":")
app.patch('/api/cart/:id', async (req, res) => {
  try {
    await sequelize.query(`
      UPDATE Cart 
      SET quantity=:quantity
      WHERE id=:id
    `,{
      logging: false,
      replacements: {
        // используем параметр из пути
        id: req.params.id,
        quantity: req.body.quantity
      }
    })
  } catch (error) {
    console.warn('ошибка при редактировании корзины:', error.message)
    res.status(500).send(error.message)
  } finally {
    res.end()
  }
})

Delete. Удаление блюда из корзины

Тут ничего нового

app.delete('/api/cart/:id', async (req, res) => {
  try {
    await sequelize.query(`
      DELETE 
      FROM Cart
      WHERE id=:id
    `,{
      logging: false,
      replacements: {
        id: req.params.id
      }
    })
  } catch (error) {
    console.warn('ошибка при удалении блюда из корзины:', error.message)
    res.status(500).send(error.message)
  } finally {
    res.end()
  }
}) 

Задание

Реализовать АПИ (CRUD всех моделей) для своего курсового проекта по образцу.