# Техническое описание проекта "ПлюсАгент". Бэкенд-часть
## Введение
Бэкенд работает на платформе NodeJS версии 10 и написан на языке TypeScript. В качестве базы данных используется MySQL.
Программная архитектура проекта завязана на использование собственного фреймворка под названием `luxe-framework`. Значительная часть данного документа посвящена описанию возможностей фреймворка. На архитектуру проекта повлияли идеологи Domain Driven Design (Предметно-ориентированное проектирование, DDD) и Clean Architecture (Чистая архитектура).
## Основные принципы
Исходный код проекта был написан с применением следующих принципов:
1. **Код фреймворка отделён от кода приложения**. Директория с исходным кодом приложения `src` содержит только код, специфичный для предметной области данного приложения. Если рассматриваемая единица кода (класс или фунция) не обслуживает данную предметную область и может быть использована в приложении с совершенно иной предметной областью, то такая единица кода расценивается как библиотека и размещается в соответствующей директории `lib`. Таким образом, **код приложения содержит только важный для предметной области код**.
1. **Код предметной области отделён от кода инфраструктуры**. Согласно принципу DDD, код, реализующий бизнес-правила данной предметной области (так называемый "доменный код") рассценивается как наиболее важный и логически отделяется от остального кода (так называемого "инфраструктурного код"). Доменный код должен быть независимым от внешних по отношению к нему ресурсам (за исключением редких случаев, когда выполнение этого принципа невозможно или нерационально). Таким образом доменный код не зависит от используемой базы данных, библиотеки ORM (ODM), HTTP-клиента, сервиса рассылки SMS и прочих ресурсов. Для отделения кода используется принцип IoC (инверсия зависимостей). Таким образом, **реализация бизнес-правил не зависит от инфраструктурных вещей, которые не имеют отношения к данным бизнес правилам**
1. **Код приложения разделён на модули**. Весь код разделён на отдельные единицы, прямые зависимости между которыми невозможны (для проверки используется библиотека `eslint`). Каждый модуль может зависить только от специальных модулей `core` (содержит наиболее общий код, используемый большинством модулей) и `shared` (содержит общие определения типов, перечисления и константы). Для взаимодействия между модулями используются специальные классы-клиенты, реализация которых определяется отдельно от модуля (по принципу IoC). Таким образом **контроллируются зависимости между частями кода, и отдельную часть кода при необходимости можно вынести в отдельный сервис**.
## Структура приложения
В корневой директории находятся следующие файлы:
- Конфигурации библиотек:
- `.eslintrc` для `eslint`
- `gulpfile.js` для `gulp`
- `ormconfig.js` для `typeorm`
- `tsconfig.json` и `tsconfig.base.json` для `typescript`
- Конфигурация окружения (см. библиотеку `dotenv`)
- `.env.dist` - базовый шаблон для создания файла конфигурации
- `.env` - сам файл конфигурации; игнорируется в Git. Для создания нужно скопировать файл `env.dist` или запустить скрипт `env.sh`
- Скрипты:
- `update.sh` - установка зависимостей и сборка приложения
- `env.sh` - см. пункт "Конфигурация окружения"
- Определения зависимостей `package.json` и команд `npm`
В корневой директории находятся следующие поддиректории:
- `src` - исходный код приложений.
- `src/app` - код, нужный для запуска приложения (в режиме веб-сервера или консольной команды). Также содержит определения клиентов модулей по принципу IoC (см. основной принцип №3)
- `src/shared` - особый модуль, содержит общие для всего приложения определения типов, перечисления и константы
- `src/core` - особый модуль, содержит наиболее общий код, используемый большинством модулей
- остальные поддиректории являются стандартными модулями приложения
- `lib` - исходный код собственных библиотек (в частности, фреймворка `luxe-framework`).
- `data` - ресурсы приложения, такие как статические файлы, логи и файлы конфигурации (в частности, файл `.appconfig`, содержащий перечень свойств приложения по аналогии с файлом `application.properties` из Java Spring Framework)
## Установка бэкенд-приложения
1. Клонировать репозиторий `queses/lx-site-rebuild` и перейти в папку `/backend`
1. Выполнить `sh env.sh`, либо сделать копию файла `env.dist` под названием `.env`
1. Создать две базы данных MySQL - одну для разработки (например, `lxnew`), и другую для тестирования (например, `lxnew_test`)
1. Наименование БД для разработки, имя пользователя и пароль пользователя записать в `.env` переменные `DB_NAME`, `DB_USER` и `DB_PASS` соответственно
1. Наименование БД для тестирования, имя пользователя и пароль пользователя записать в `.env` переменные `DB_NAME_TEST`, `DB_USER_TEST` и `DB_PASS_TEST` соответственно
1. Записать в `.env` случайные ключи шифрования, например:
```
JWT_SIGN_KEY = ZOPwAn76LxSzxTpraIHmBsbfRUeAOvyO
CRYPTO_KEY_JWT = ItAux_tRqoSkuxY3vAsdAElO
CRYPTO_IV_JWT = 0CV58Tn4JnvXpdst
```
7. Выполнить миграцию БД для разработки командой `yarn orm schema:sync`
7. Выполнить миграцию БД для тестирования командой `yarn orm:test schema:sync`
7. Проверить, что выполняются тесты командой `yarn test`
7. Запустить веб-сервис командой `yarn dev`
## Компоненты приложения
Компонент - это класс, объект который можно внедрить в объект другого компонента по технологии Dependency Injection (внедрение зависимостей), либо получить из контейнера (класс `AppContainer`). Существуют три основных типа компонентов:
### Singleton
Определяется с помощью аннотации `@SingletonService`. Каждый раз при получении из контейнера возвращается один и тот же объект (т. е. объект Singleton-компонента за время жизни приложения создаётся ровно один раз).
### Transient
Определяется с помощью аннотации `@TransientService`. Каждый раз при получении из контейнера возвращается новый объект (т. е. объект Transient-компонента за время жизни приложения создаётся ровно столько раз, сколько он был получен из контейнера).
### Context
Определяется с помощью аннотации `@ContextService`. Является разновидностью Transient-компонента, т. е. создаётся каждый раз при получении из контейнера.
Отличительной чертой контекстных компонетов является наличие контекста, в который записывается необходимая информация о происходящем процессе.
Понимание принципа контекстных компонентов важно для понимания всего приложения в целом. Необходимость существования подобного типа сервисов заключается в особенностях языка JavaScript.
Для примера сравним веб-приложение на NodeJS с веб-приложениями на Java и PHP.
При поступлении HTTP-запроса (или иного сетевого запроса) в приложение на PHP веб-сервер (например, `httpd` или `nginx`) создаёт новый процесс в операционной системы (опустим из внимания различный оптимизации, выполняемые веб-серверами). Таким образом, каждый запрос выполняется в рамках отдельного процесса, и информацию о контексте запроса можно получить из различных глобальных переменнах (например, `$_GET`).
При поступлении запроса в приложение на Java контейнер сервлетов (например, `Tomcat` или `Jetty`) выбирает для его обработки свободный поток. Таким образом, каждый запрос выполняется в рамках отдельного потока, и информацию о контексте запроса можно получить, например, с помощью компонента `HttpServletRequest` (в Spring Framework).
Приложение на NodeJS в свою очередь выполняется в рамках одного запроса, и информацию контексте текущего запроса невозможно получить нигде, кроме самого обработчика запроса.
Чтобы обойти данную особенность, в приложении используется концепция контекстных компонентов. Каждый раз в обработчике запросов (будь это сетевой запрос или консольная команда) создаётся объект, хранящий контекст текущего запроса. Этот объект заполняется необходимыми данными из запроса (например, ID текущего пользователя системы). Затем этот контекст передаётся во все контекстные конпоненты, задействованные в обработке данного запроса.
Обработчики запросов расположены в контроллерах. Рассмотрим на следующем примере стандартный путь запроса в приложении:
1. Поступает запрос; пусть, к примеру, это будет POST HTTP-запрос на создание объекта недвижимости, распложенный по адресу `/api/realty/object`; запускается нужный метод контроллера
1. Система создаёт объект контекста из запроса; пусть, например, в контекст сохранится ID текущего пользователя и список его полномочий (для передачи этих данных от клиента к серверу в приложении используется JWT; таким образом, для построения контекста системе не придётся обращаться в БД)
1. Обработчик получает из контейнера контекстный компонент `CreateRealtyObjectForm`, расположенный в слое предметной области и ответственный за создание объекта недвижимости из данных формы и сохранение его в БД. У этого сервиса контроллер вызывает нужный метод (скажем, `create`) и передаёт в него данные формы
1. Метод `create` прежде всего обращается к контексту и проверяет, задан ли текущий пользователь (то есть был ли осуществлён вход в систему) и есть ли у этого пользователя необходимые полномочия
1. В случае неудачи система выкидывает ошибку `DomainAccessError`, которая означает недостаток полномочий для выполнения бизнес-процесса. В случае появления этой ошибки контроллер возвращает ответ с HTTP-статусом `403 Forbidden`.
1. В случае, если прав достаточно, выполнения бизнес-процесса продолжается, и контроллер возвращает результат с HTTP-статусом `200 OK`.
#### Виды контекстных компонентов
В системе помимо обычных контекстных компонетов используются две их дополнительных разновидности:
- UseCase (вариант использования) - класс, обозначенный аннотацией `@UseCase` и предназначенный для выполнения ровно одного вида действий. Необходимое условие - это наличие метода `run`, с помощью которого и запускается UseCase. Рекоммендуется описывать бизнес-процессы с помощью UseCase.
- ReadService (сервис для чтения) - класс, специально предназначенный для чтения нужных данных из БД. Стандартный метод ReadService'а выполняет следующие действия: проверяет полномочия пользователя, осуществляет чтение из репозитория (промежуточного класса для работы с БД) и, при необходимости, выполняет преобразование данных.