/RSREU-training_Integration

:mortar_board: methodical material [RU]

Primary LanguageJavaScript

Лабораторная работа №3

Интеграция пользовательского интерфейса с базой данных

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

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

Шаг 0️⃣ -- Подготовка проекта

В качестве отправной точки мы будем использовать итоговый проект из Лабы №1, с немного изменненной структурой. Для начала установим npm-зависимости. Для этого в командной строке выполним команду

  npm install

В данной лабе мы подключаем клиентскую часть, как зависимость проекта (library-ui в package.json), и нам не нужно запускать ее отдельной командой. После установки зависимостей и автоматической сборки клиентского приложения нам остается только запустить наш сервер командой

  npm start

после чего перейдем по адресу http://localhost:8080, где должна появиться наша коллекция книг.

P.S. В package.json в поле "scripts" определена команда start:dev. Эта команда использует библиотеку nodemon, которая позволяет существенно ускорить процесс разработки node-приложений (вам не нужно будет перезапускать сервер каждый раз после внесения изменений в код!). Запустите ее вместо команды start следующим образом: npm run start:dev.

Шаг 1️⃣ -- Подключение MongoDB

Для того, чтобы обеспечить взаимную работу нашей БД с сервером необходимо установить специальный npm-пакет mongodb, который является официальным драйвером MongoDB для Node.js и предоставляет высокоуровневое API поверх mongodb-ядра. Скорее всего он у вас уже установлен. Для этого загляните в файл package.json (объект "dependencies"), если же нет, введите в консоли следующую команду

  npm install --save mongodb

Теперь необходимо подключить установленный пакет к проекту. Для того, чтобы использовать объект нашей БД в любом месте проекта вынесем логику взаимодействия с ним в отдельный модуль: создадим папку ./db в корне проекта, а т.ж файл index.js в ней.

Импортируем нужную нам библиотеку с помощью подхода CommonJS (функции required). В данном случае нас интересует объект MongoClient, который позволит установить соединение с БД, а затем при ее успешном установлении получить объект БД.

Ниже приведен код модуля, который экспортирует метод connect, который принимает на вход два парамера -- переменную, в которой хранится путь к БД и функцию-callback (обратного вызова), затем пробует установить соединение, и кладет в переменную _db объект БД, если соединение установлено, далее вызывается callback. Перенесите этот код в свой модуль.

const MongoClient = require('mongodb').MongoClient;
let _db = null;

module.exports = {
  connect(url, callback) {
    return MongoClient.connect(url, (err, database) => {
      if (!err) {
        _db = database;
      }
      callback(err, database);
    });
  },
};

Важно! В данном модуле мы используем подход callback-функции, т.е. передаем функцию в качестве одного из параметров функции, и вызываем ее, когда завершился процесс установки соедиения с БД. Данный подход один из возможных вариантов работы с асинхронностью в JavaScript. Этот подход мы будем использовать далее в лаботаторной работе.

Отлично, теперь переменная _db хранит объект базы данных. Но сейчас он не очень полезен, т.к используется только в текущем модуле. Целесообразно будет написать метод, который бы отдавал нам этот объект.

Задание 1.1

Напишите метод getInstance (по примеру #connect), который будет отдавать объект БД, если тот имеется, в противном случае, выбрасываем ошибку, что соединение прервано.

Для удобства добавим метод getCollection, который будет принимать название коллекции БД, и возвращать объект этой коллекции соответственно.

module.exports = {
  ...
  getCollection(name) {
    return this.getInstance().collection(name);
  }
}

P.S. Если коллекция не найдена, MongoDB неявно создает новую коллекцию самостоятельно.

Далее Вам необходимо запустить сервер mongod и оболочку mongo как в прошлой лабе.

Шаг 1 почти завершен, осталось выполнить само подключение, используя наш модуль db. Перейдем в файл "server.js", подключим наш модуль, а т.ж зададим URL нашей БД в константе.

const MongoDB = require('./db');
const DB_URL = 'mongodb://localhost:27017/library';

Далее используя написанный ранее метод connect установим соединение с БД. В функции-callback'е проверим, что процесс установки соединения завершился без ошибок, и запустим сервер, в противном случае выводим ошибку соединения и завершаем программу.

// импорт зависимостей
...

MongoDB.connect(DB_URL, (err, database) => {
  if (err) {
    return console.log(`An error with connection to db ${err}!`);
  }

  console.log(`Connected to DB succsessfully!`);
  
  // код имеющейся серверной части
    ...
});

Если все сделано правильно, то в консоли вы увидете следующую информацию:

  [nodemon] starting `node server.js`
  Connected succsessfully!
  Server is running: localhost:8080

Важно! Существует ряд соглашений о том, как вызывать callback-функции. В среде Node.js самый распространенный шаблон -- "error-first" ("node-style callback"), т.е. первым параметром функции всегда является объект ошибки. Подробнее об этом можно прочитать здесь.

Шаг 2️⃣ -- Создание сервисов

В предыдущих лабораторных работах вами были созданы несколько путей для доступа к API (./api/index.js). Сейчас они предоставляют фиктивные данные, взятые из JSON. Наша задача -- создать модуль, который будет иметь доступ к объекту БД и вытаскивать из нее необходимые данные, согласно запросам с клиента.

Рассмотрим пример с созданием сервиса для коллекции книг.

Создадим папку ./services, и файл book.service.js. Получим объект коллекции "books" из нашей БД ./db/index.js с помощью #getCollection и для удобства запишем его в отдельную переменную.

const bookCol = require('../db').getCollection('books');

Далее создадим объект сервиса, который в дальнейшем будет пополняться методами для работы с БД, а т.ж выполним его export. На примере запроса получения всех книг - GET /api/books напишем метод, который будем использовать в API:

const service = {
  getAllBooks(callback) {
    return bookCol.find({}).toArray(callback);
  },
};

module.exports = service;

Синтаксис запросов с помощью mongodb очень похож на mongo-shell, используемый вами на предыдущей лабе. Однако, вы могли заметить метод "toArray". Дело в том, что метод find возвращает не данные, а объект Cursor - указатель на результирующий набор данных, полученных с помощью запроса. Но нам нужны документы, а не указатель. Для этого и используется метод "toArray".

Подробнее про cursor можно прочитать здесь, а про метод toArray здесь.

Важно! Драйвер mongodb позволяет использовать 2 вида управления асинхронными операциями:

  • callback, передаваемый в используемую функцию
  • возвращая Promise из используемой функции

Выполняя запрос к БД мы также выполняем асинхронную операцию. Ранее в этой лекции приводилось описание применения callback-функции. В данном случае callback, переданный в метод toArray будет вызван со следующими аргументами: 1 - ошибкой (да, mongodb т.ж использует "error-first" подход), 2 - массив документов.

Далее импортируем наш сервис в файл с API и используем его по-назначению:

...
const bookService = require('../services/book.service');

router
  .get('/books', (req, res) => {
    bookService.getAllBooks((err, books) => {
      if (err) {
        console.error(err);
        return res.send(err);
      }

      return res.send(books);
    });
  })
  ...

Теперь, если мы обновим страницу, то увидим список книг, хранящихся в БД.

Отлично! У нас написан сервис для коллекции книжек. Но это еще не все 😬

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

🎭 Задания по вариантам

Задание 2.1

По примеру создания bookService, написать сервис для коллекции:

  • "categories" -- четный вариант
  • "filters" -- нечетный вариант

который будут иметь единственный метод getAll(Categories|Filters) и подключить его к соответствующим ручкам роутера.

P.S. т.к эти сервисы понадобятся каждому, код сервиса другого варианта можно стащить у соседа 💩, или написать самому 👍.

P.P.S. теперь можно удалить более не используемые импорты моделей JSON в api/index.js.

Шаг 3️⃣ -- Обработка составных запросов. Фильтрация

Как вы могли заметить, на нашем клиенте имеется возможность ввести что-либо в стоку поиска, выбрать категорию или фильтр. Чтобы сообщить серверу о таких дополнительных параметрах, клиент в процессе GET-запроса может передавать параметры выполнения в URI ресурса после символа ?. В нашем случае дополнительными параметрами строки запроса являются:

  • search -- символы строки поиска
  • activeFilter -- тип выбранного фильтра
  • activeCategory -- тип выбранной категории

Каждый из параметров не обязательно может быть выбран/введен, в этом случае он не отправляется.

Для получения этих параметров запроса в Express.js используется объект request функции обработчика .get('/', (reqest, response) => ...), а именно его свойство .query.

Важно! Не путать request.query и request.params:

  • params

    GET /api/books/1
    app.get('/books/:id', (reqest, response)=>...)
    
    request.params.id // 1
    
  • query

    GET /api/books?search='Figth Club'
    app.get('/books', (reqest, response)=>...)
    
    request.query.search // Figth Club
    

Подробнее о query здесь, о params тут.

На примере, описанного ранее запроса для получения всех книг GET /api/books, добавим поддержку выборки с фильтрацией. В нашем случае запрос GET может содержать 3 параметра, т.е для того, чтобы на сервере проверить одно из возможных состояний фильтрации, нам необходимо написать 2^3 проверок (каждый фильтр может быть как в активном состоянии, так и не активен -- не отправляться). Для исключения написания большого boilerplate на проверку всех условий, в проекте уже имеется функция-утилита, которая будет выполнять для каждой книги проверку на соответствие каждому из фильтров. Если фильтр не указан -- проверка опускается.

Эта утилита содержится в папке ./utils в корне проекта, а именно в файле meet-query.utils.js.

Выполним импорт нашей функции в файле ./api/index.js, и в обработчике нашего GET запроса, предварительно получив список всех книг, проверим каждую из них с помощью новой супер-утилиты 🚀:

// GET /api/books handler
...
  // получим параметры фильтрации из query
  const searchString = req.query.search;
  const activeFilter = req.query.activeFilter;
  const activeCategory = req.query.activeCategory;

  bookService.getAllBooks((err, books) => {
      if (err) {
        console.error(err);
        return res.send(err);
      }
      // отфильтруем книги
      const requiredBooks = books.filter(book =>
        // с помощью утилиты meetQuery
        meetQuery(book, search, activeFilter, activeCategory));

      return res.send(requiredBooks);
    });
...

Отлично, теперь мы можем фильтровать нашу коллекцию! Проверьте это в приложении. Ознакомительная практическая часть закончена. Далее приведены самостоятельные задания, которые основаны на пройденном материале.

Самостоятельные задания

Необходимо написать функцию-обработчик POST-запроса /api/book в зависимости от параметра action (добавление, изменение или удаление книги).

Важно! Дополнительные параметры в POST-запросе передаются в теле запроса (объект request.body). Подробнее про POST можно почитать здесь.

Для каждого задания указана схема тела запроса (объекта body), т.е то, что нам отправляет в запросе клиент.

Под функцией-обработчиком в данном контексте понимается функция, которая выполняет следующие действия:

  1. с помощью типа action и параметров указанных в теле запроса, а т.ж. функции одного из сервисов, изменяет данные, хранящиеся в БД
  2. отправляет на клиент список всех книг (с изменениями, указанными в п.1), удовлетворяющий фильтрам, указанным в теле запроса

P.S. Параметр _id, который пердается с клиента, представляет собой шестнадцатеричный код, который MongoDB генерирует при создании объета автоматически. Для выбора необходимой книги по такому id необходимо будет импортировать функцию .ObjectID из пакета mongodb, и использовать ее как обертку над принятым id, прим. {"_id": ObjectID(_idFromBody)}. Подробнее об ObjectId.

Задание №1. CREATE

Обработчик запроса на добавление книги (кнопка ADD A BOOK).

// request.body schema
{
  action: 'create',
  book: {
    title: string,
    author: {
      firstName: string,
      lastName: string,
    },
    categories: string[],
    keywords: string[],
    img: string,
  }
  search: string | undefined,
  activeFilter: string | undefined,
  activeCategory: string | undefined,
}

Задание №2. UPDATE

Обработчик запроса на изменение книги (нажать на книгу, затем изменить данные и нажать submit, клик на звезды под книгой).

// request.body schema
{
  action: 'update',
  book: {
    _id: string,
    title: string,
    author: {
      firstName: string,
      lastName: string,
    },
    categories: string[],
    keywords: string[],
    img: string,
  },
  search: string | undefined,
  activeFilter: string | undefined,
  activeCategory: string | undefined,
}

Задание №3. DELETE

Обработчик запроса на удаление книги (клик на книгу, затем кнопка DELETE в модальном окне).

// request.body schema
{
  action: 'delete',
  _id: string,
  search: string | undefined,
  activeFilter: string | undefined,
  activeCategory: string | undefined,
}

The End.

🎉