Легкий фреймворк для Node.js приложений.
Логика приложения определяется композицией сервисов. Сервис — это простейший javascript класс с любым набором методов для реализации логики приложения.
Связующим звеном между сервисами является менеджер сервисов. Менеджер сервисов - это объект с набором методов для доступа к любому сервису приложения. Методы менеджера возвращают экземпляр конкретного сервиса.
Каждый сервис создаётся в единственном экземпляре при первом обращении
к нему. Если у сервиса реализован асинхронный метод async init()
, то он будет вызван при первом
обращении к сервису. Метод init()
позволит выполнить асинхронную логику перед использованием сервиса,
в этот метод передаются параметры конфигурации сервиса и менеджер сервисов.
Через менеджер сервисов можно обратиться к любому другому сервису не задумываясь о его инициализации. Разработав новый сервис, необходимо прописать типовой метод доступа к нему в менеджере сервисов.
При старте приложения осуществляется инициализация менеджера сервисов, передача ему всех конфигураций про все сервисы. Далее выполняется обращение к конкретному сервису и вызов его методов. Первый вызванный сервис оказывается входной точкой логики приложения. Например, запускается сервер REST API.
Универсальная логика старта приложения - это запустить сервис по параметрам CLI, в этих параметрах и
название вызываемого сервиса. По сути можно запустить любой сервис. Для универсального старта
сервисов используется метод async start()
. Если этот метод есть в сервисе, значит сервис можно
запустить из командной строки ничего дополнительно не настраивая.
Главный файл приложения:
index.js
const Services = require('./services'); // Менеджер сервисов
const {parseCommands} = require('./utils/arrays'); // Парсер парамтров CLI
(async () => {
// 1. Создаём менеджер сервисов
const services = new Services();
// 2. Иницализируем менеджер с передачей массива конфигурации
// Вместо параметров можно указать строку - путь на файл с конфигурацией
// Конфигурация - это параметры для каждого сервиса, сгруппированные по названиям
await services.init(['configs.js', 'configs.local.js', {'rest-api': {exampleOption: true}}]);
// 3. Запуск сервиса по параметрам CLI
await services.start(parseCommands(process.argv.slice(2)));
})();
Для разбора параметров командой строки используется парсер без декларирования схемы параметров. Например:
> node index.js name --log mode=super list[]=10,20 second --log=false
name
- название первого сервиса и его параметры{log: true, mode: 'super', list: [10,20]}
.second
- название второго запускаемого сервиса и его параметр{log: false}
Вместо запуска по параметрам CLI, можно явно вызвать желаемый сервис
(async () => {
//...
// 3. Явный запуск сервиса rest-api
const restApi = await services.getRestAPI();
await restApi.start();
})();
В exser реализованы часто используемые сервисы для типового REST API приложения.
const storage = await services.getStorage()
Хранилище данных в MongoDB. Позволяет задекларировать схемы моделей по спецификации JSONScheme, валидировать, сохранять, выбирать, удалять объекты из mongodb. Реализует отношения между объектами, мультиязычные свойства, упорядочивание, древовидные структуры, подтягивание свойств связанных объектов и другое.
Каждая модель описывается отдельным javascript классом, по сути тоже сервисом, разве что доступ к
ним осуществляется через storage. Подключение классов моделей осуществляется в параметре models
сервиса storage. Для удобства файлы моделей можно вынести в отдельную папку /models
и подключать её.
const users = storage.get('user'); // указывается название (тип) модели
const list = await users.getList({filetr: {name: 'admin'}});
const restApi = await services.getRestAPI()
HTTP сервер с описанием роутеров под REST API. Используется библиотека express, сервис декорирует её. При объявлении роутеров в методах get, post, put, patch, delete введен дополнительный параметр для описания спецификации роутера в OpenAPI 3.0. Автоматически собирается докуменатция в swagger, она становится доступной по адресу /api/v1/docs.
Реализована спецификация Query REST, с возможностью указывать, какие поля объекта выбирать или не выбирать, подтягивать свойства связанных объектов в рамках одного запроса, гибко указывать параметры фильтра, сортировки и другое.
Сервисом rest-api стандартизируется формат ответа и обработка ошибок, реализуется первичный контроль доступа.
Роутеры описываюся отдельными файлами в функции для замыкания в ней менеджера сервисов. Обработка запроса - это вызов методов какого-либо сервиса.
module.exports = async (router, services) => {
const spec = await services.getSpec();
const storage = await services.getStorage();
const users = storage.get('user');
/**
* Создание/регистрация
*/
router.post('/users', {
// OpenAPI 3.0 сецификация роута
operationId: 'users.create',
summary: 'Регистрация (создание)',
description: 'Создание нового пользователя (регистарция).',
tags: ['Users'],
requestBody: {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/user.create'
}
}
}
},
parameters: [],
responses: {
201: spec.generate('success', {$ref: '#/components/schemas/user.view'}),
400: spec.generate('error', 'Bad Request', 400)
}
}, async (req, res) => {
return await users.createOne({
body: req.body,
session: req.session
});
});
// ..остальные роуты про user
};
Файлы с описанием роутеров подключаются к сервису rest-api через конфигурацию в параметре routers
.
Роутеры можно кастомизировать не затрагивая класс самого сервиса.
const spec = await services.getSpec()
Сервис для описания спецификаций и валидации данных по ним. Спецификация — это схема в формате JSONScheme. Сервис группирует все объявленные схемы в единую структуру. Предоставляет утилиты, готовые фрагменты схем для упрощения описания целевой схемы. Используется в описании моделей сервиса storage, а также в роутера сервиcа rest-api и генерации спецификации openapi 3.0.
Расширение сервиса - это добавление своих утилит, заготовок. Все они прописываются в параметрах конфигурации сервиса.
const sessions = await services.getSessions()
Сервис для работы с сессией. Одно приложение может одновременно обрабатывать запросы от разных клиентов. Сервис сессий используется для создания объекта состояния сессии. В состояние обычно попадает объект текущего авторизованного пользователя, параметры локали и любые другие данные, специфичные для приложения. Состояние сессии обычно создаётся в обработчике всех http запросов и присваивается к объекту запроса. Объект состояния сессии передаётся практически во все методы сервисов.
const restApi = await services.getTasks()
Сервис для периодического запуска сервисов. В конфигурации описывается задача - название сервиса, сколько раз запускать или с какой периодичностью. Используется для выполнения плановых задач, например синхронизации с внешними службами каждые 5 минут. При старте сервиса tasks указывается название выполняемой задачи.
> node index.js tasks --name=example
В конфигурации
tasks: {
example: {
service: 'example', // сервис
interval: 500, // интервал ms
iterations: 3, // кол-во
someOption: 'xxx', // переопределение параметра для сервиса example
log: true
},
},
const restApi = await services.getDump()
Сервис экспорта данных из storage в файлы и обратно. В дамп не попадают суррогатные ключи (_id), чтобы восстановление происходило по постоянным ключам. При повторном экспорте данных в файл, сущесвующий файл будет перемещен во временную папку операционной системы. При особых случаях можно обратится к прошлым дампам. Дамп создают и обрабатываются в потоковом режиме. Формат файла: одна строка - один объект в JSON.
Экспорт в файл
> node index.js dump --export[]=user,role
Импорт в storage
> node index.js dump --import[]=user,role
Какие модели экспортировать или импортировать по умолчанию можно указать в конфигурации сервиса.
const restApi = await services.getLogs()
Сервис логирования с учётом сессии. Альтернатива классическому console.log(). Отличительная осбенность - вывод лога одной строкой и группировка логово по сессии. Удобен при сборе логов консоли в графану.
Фреймворк exser используется через наследование его менеджера сервисов и при необходимости его встроенных сервисов. Для этого в проекте объявляется свой класс менеджера сервисов наследованием менеджера exser. Именно его нужно создавать и через него запускать сервисы приложения, в нем объявлять методы доступа к своим сервисам или переопределять методы доступа к встроенным сервисам.
Например, для расширения сервиса rest-api с целью подключение дополнительных middleware к express, необходимо объявить свой класс RestAPI наследованием из exser, его прописать в методе getRestAPI в менеджере сервисов. В классе RestAPI переопределить метод start.
const exserRestApi = require('exser/services/rest-api');
class RestAPI extends exserRestApi{
async start(params = {atFirst: null, atEnd: null, atError: null, atRequest: null, atResponse: null}) {
return super.start({
atFirst: async app => {
// здесь можно подклчить middleware к app перед всеми другими middleware
},
atEnd: async app => {
// здесь можно подклчить middleware к app после всех других middleware
},
})
}
}
module.exports = RestAPI;