README
Small RPC
Простой RPC для проекта. Можно использовать как с HTTP, так и с сокетами, так и с любым другим транспортом. Вытащен и допилен из RPC Redbone, работает на одном с ним протоколе.
Установка
npm install small-rpc
или через Yarn
yarn add small-rpc
Использование на сервере
Важно понимать, что библиотека использует async
/await
и вам понадобится NodeJS версии 8 или выше.
Подключение
const RPC = require('small-rpc');
const rpc = new RPC();
// Добавляем объект для вызова (module):
rpc.setModule('profile', new Profile());
const { json, send } = require('micro');
// вызов механизма rpc, например, внутри модуля micro
module.exports = async (req, res) => {
const action = await json(req, { limit = '0.3mb', encoding = 'utf8' });
try {
send(res, 200, await rpc.call({ req, res }, action));
} catch(err) {
send(req, 500, 'Internal Server Error');
}
};
action
Валидация RPC валидирует action
самостоятельно, и по-умолчанию стреляет обычным Error
, если с action
что-то не так.
Чтобы более точно отлавливать такие ошибки можно использовать свой наследник от Error
.
class MyError extends Error {};
RPC.Error = MyError;
const { json, send } = require('micro');
module.exports = async (req, res) => {
const action = await json(req, { limit = '0.3mb', encoding = 'utf8' });
try {
return send(res, 200, await rpc.call({ req, res }, action));
} catch(err) {
if (err.constructor === MyError) {
return send(req, 400, MyError.message);
}
return send(req, 500, 'Internal Server Error');
}
};
Добавление библиотеки модулей
Вы можете добавлять объекты пачками, и разделять их на библиотеки.
const RPC = require('small-rpc');
const rpc = new RPC();
// Добавляем модели mongoose в RPC
rpc.setLib('mongoose', mongoose.models);
Вы также можете дополнить любую библиотеку еще одним модулем:
// Добавит модуль Profile в библиотеку mongoose
rpc.setModule('mongoose.Profile', Profile);
Если вы добавляете модуль, без указания библиотеки, он добавляется в библиотеку по-умолчанию — main
.
Также если вызвать setLib
с одним аргументом, и передать просто объект он будет записан как библиотека main
.
// Записываем модели mongoose как библиотеку `main`
rpc.setLib(mongoose.models);
// Добавляем объект для вызова profile (из которого мы будем вызывать методы) в библиотеку `main`
rpc.setModule('profile', new Profile());
Middleware
Вы можете использовать middleware
-функции, чтобы определять уровни доступа и дополнительную бизнес-логику.
Middleware
бывают четырех типов:
- Все запросы
- Для конкретной библиотеки
- Для конкретного модуля
- Для конкретного метода
Все они задаются через метод use
rpc.use((payload, action) => {
// Выполнится для всех запросов
});
rpc.use('main', (payload, action) => {
// Выполнится для всех запросов к библиотеке `main`
});
rpc.use('mongoose', (payload, action) => {
// Выполнится для всех запросов к библиотеке `mongoose`
});
rpc.use('mongoose.Profile', (payload, action) => {
// Выполнится для всех запросов к библиотеке `mongoose` и модулю `Profile`
});
rpc.use('mongoose.Profile.login', (payload, action) => {
// Выполнится для всех запросов к библиотеке `mongoose` и модулю `Profile` при вызове метода `login`
});
При вызове в middleware
передается:
payload
— формируется при вызове rpc
action
— объект действия rpc
rpc
— экземляр rpc
который вызывает middleware
Чтобы остановить поток выполнения middleware
нужно просто вернуть false
из той middleware
на которой вы хотите остановиться.
rpc.use((payload, action) => {
if (!action.jwt) return false;
});
Также middleware
могут быть объектами. Чтобы использовать такую middleware
нужно определить в объекте метод call
, аналогичный функциональной версии.
Это может быть полезно, если middleware
большая, и её хочется разделить на части, или если у нее есть параметры и состояние.
Middleware до и после выполнения метода
Все middleware
, которые вы добавляете методом use
выполняются до выполнения метода.
Чтобы добавить немного семантики в код их подключения можно использовать поле before
.
rpc.before.use(someMiddleware);
У rpc.before.use
такой же интерфейс, как и у use
.
Если вам необходимо выполнить какую-то проверочную логику после того, как метод отработал, то для этого используется rpc.after.use
. Такие middleware будут выполняться после того, как метод отработал и был сформирован итоговый action
. Интерфейс добавления after-middleware
такой же как и у обычных middleware
, за исключением того, что четвертым аргументом в нее передается подготовленный итоговый action
.
rpc.after.use(someAfterMiddleware);
Остановка в такой middleware
приводит к тому, что все дальнейшие middleware
добавленные в after
не будут выполнены, а из rcp.call
вернется undefined
вместо action
.
Это отличное место, чтобы добавить реакции на возвращаемые данные, или обогатить action
, или записать что-то в сессию клиента.
action
Возвращаемый После того как механизмы RPC отработают, метод call
вернет специальный объект — action
, который можно сразу отправить клиенту. Его анатомия:
{
"type": "@@client/rpc/RETURN",
"id": "ID от запроса, если он был",
"payload": "То что вернул метод"
}
Если во время выполнения middleware
были остановлены, то call
вернет undefined
, вместо готового action
.
Любой метод из модуля будет вызван с await
, то есть методы могут возвращать Promise
и в ответ попадет результат его штатного разрешения.
Если вы хотите, чтобы при возникновении ошибок автоматически генерировался возвращаемый action
, используйте safeCall
вместо обычного call
. В таком случае при возникновении ошибки, которую можно перехватить, т. ч. асинхронной, будет сгенерирован action
:
{
"type": "@@client/rpc/ERROR",
"id": "ID от запроса, если он был",
"payload": {
"message": "Сообщение ошибки",
"code": "Eсли у ошибки был код, то он тут будет"
}
}
type
— может измениться в зависимости от входящегоaction
.
Вы можете переопределить метод генерации ответов, для этого нужно заменить методы:
makeOutAction(result, inAction)
— для успешных вызововmakeErrorAction(error, inAction)
— если возникла ошибка
action
Анатомия входящего {
"type": "@@service/rpc/CALL",
"id": "F12AE83",
"lib": "main",
"module": "main",
"method": "echo",
"arguments": ["Hello Redbone RPC"],
"flat": true,
"backType": null,
"merge": false,
"filter": null,
"errorType": null
}
id
— запроса, задается клиентом. Будет прикладываться ко всем ответам протокола. Допустима строка до 24 символов, можно передать число, но в ответе, оно будет передано как строка.lib
— имя библиотеки объектов,main
— библиотека по-умолчаниюmodule
— имя модуля в библиотеке —main
— модуль по-умолчаниюmethod
— имя метода в библиотеке — обязательное полеarguments
— любое значение которое будет передано в качестве аргумента метода, который будет вызванflat
— если вarguments
передать массив, аflat
задать какtrue
, то массив будет разложен по аргументам методаbackType
— тип который будет передан в ответномaction
, еслиnull
или поле не задано, то будет использован тип по-умолчаниюmerge
— если передать какtrue
, то ответные данные будут лежать в корне действия, а не поляpayload
. Учтите, чтоtype
иid
могут быть перекрыты в этом случае. Если результатом выполнения метода оказался не объект, тоmerge
не будет иметь действия, и данные все равно будут находиться внутри поляpayload
filter
— массив полей, которые нужно вернуть, если в ответе от метода был получен объектerrorType
— тип который будет передан в ответномaction
когда произойдет ошибка вызова (catch любой ошибки). Если задать какnull
будет использован тип по-умолчанию. (Только приsafeCall
или ручном модерировании ошибок).
Middleware из коробки
Для удобства в директории /middlewares
есть несколько готовых middleware
(пока только одна).
Whitelist
Белый список: ограничивает библиотеки/модули/методы, который могут быть вызваны.
Чтобы её использовать, нужно создать экземляр класса, и передать в качестве аргумента список доступных методов:
const whitelist = new Whitelist([
'main.mail.send',
'main.mail.get',
'main.filters',
'db.logs',
'monitoring'
]);
// Подключаем middleware
rpc.use(whitelist);
Список можно подключать не только массивом, но и объектом:
const whitelist = new Whitelist({
main: {
mail: {
send: true,
get: true
},
filters: true
},
db: {
logs: true
},
monitoring: true
});
// Подключаем middleware
rpc.use(whitelist);
Если очень хочется, то можно задать и через Map:
const whitelist = new Whitelist(
new Map([
['main.mail.send', true],
['main.mail.get', true],
['main.filters', true],
['db.logs', true],
['monitoring', true]
])
);
// Подключаем middleware
rpc.use(whitelist);
Когда белый список формируется в виде объекта или словаря, в качестве значений узлов можно использовать функции.
Эти функции будут вызваны как middleware
, а результат их выполнения будет использоваться для определения доступен метод/модуль/библиотека или нет.
Важно: такая функция должна возвращать булево значение, или оно будет приведено, то есть undefined
будет расцениваться как false
в middleware
.