Соединение маршрутов
#
Мосты (Bridge) и прослойки (Middleware)Прослойки создаются при помощи декоратора @Middleware()
. Он не принимает аргументов, и просто позволяет
использовать указанный метод как промежуточный слой к любому другому элементу маршрутного узла:
конечной точкой, мостом, другой прослойкой или маршрутным узлом целиком.
Подключение middleware
происходит при помощи декоратора @Use()
, принимающего в качестве аргументов
последовательность middleware
-функций: @Use(Root.Init, Auth.Required, Users.Init)
.
Декоратор @Use()
может быть применен к endpoint-у, маршрутному узлу целиком, другой прослойке или мосту.
Все прослойки всегда выполняются перед элементом, к которому они применены.
Для соединения маршрутных элементов между собой используются мосты, создаваемые декоратором @Bridge
.
Аргументами к декоратору являются:
prefix: string
- адресный префикс маршрута, может содержать параметр, контекстный целевому маршрутному элементуnextRoute: Function
- следующий маршрутный узел: задекорированный класс, который может содержать другие мосты, прослойки иendpoint
-ы
Декоратор @Bridge
может применяться как к классу, так и к методу класса. В последнем случае метод класса
выступает как прослойка к подключаемому маршруту.
Пример:
// ... index.tsimport { Get, Bridge, Use, Middleware } from "aom";import logger from "logger";import Files from "./files";
@Bridge("/files", Files) // маршрутный узел Files доступен по префиксу `/files` относительно текущего узлаclass Index { @Get() @Use(Root.Logger) // перед методом `GET /` будет использована прослойка с логированием static Hello() { return `Hello, I'm aom`; }
@Middleware() static Logger(@Ctx() ctx, @Next() next) { logger.debug(ctx); return next(); }}
// Files.tsimport fs from "fs";import path from "path";import { Get, Bridge, Params, StateMap, Next, Err } from "aom";import FileInfo from "./fileinfo";
class Files { @Get() static Index() { return fs.readdirSync(__dirname); }
@Bridge("/:filename", FileInfo) // ожидает параметр - имя файла - в качестве следующего фрагмента пути static prepare( @Params("filename") filename: string, @StateMap() stateMap: WeakMap<any, any>, @Err() err, @Next() next ) { // получим полное имя файла с учетом директории filename = path.join(__dirname, filename); // если файл найден if (fs.existsSync(filename)) { // создает экземпляр маршрутного узла const fileInfo = new FileInfo(); // сохраняет в него полученное имя файла fileInfo.filename = filename; // и сохраняет в stateMap (контекстный объект WeakMap) stateMap.set(FileInfo, fileInfo); return next(); } else { return err("file not found", 404, [filename]); } }}
// ... fileinfo.tsimport getFileInfo from "get-file-info";import fs from "fs";
@Use(FileInfo.Init) // перед всеми методами узла выполняется прослойка `FileInfo.Init`class FileInfo { filename: string; // полное имя файла info: any; // информация о файле
@Get() static Index(@Ctx() ctx, @This() _this: FileInfo) { // установим тип возвращаемого контента согласно mime-type файла ctx.set("Content-Type", _this.info.type); return fs.readFileSync(_this.filename); }
@Delete() static Delete(@This() { filename }: FileInfo) { fs.unlinkSync(filename); return `file ${filename} successfully removed`; }
@Middleware() static Init(@This() _this: FileInfo, @Next() next) { // поскольку заведомо точно известно, что данный файл существует // то получаем о нем информацию без проверок на ошибки _this.info = getFileInfo(_this.filename); return next(); }}
Мост может быть подключен с префиксом /
: в этом случае все методы подключаемого узла будут находиться
в адресном пространстве узла, к которому происходит подключение.
Важно: при сборке все мосты подключаются после endpoint
-ов текущего узла. Таким образом, если вдруг
возникнет коллизия в значениях url
и/или prefix
, то приоритет останется за методами, подключенными
последними, то есть через @Bridge
. Разработчик обязан самостоятельно следить за адресным пространством,
которое он использует.
#
MarkerДекоратор @Marker()
позволяет обогатить информацию о точке назначения в маршрутной карте, указав,
что для элемента route
в цепочке предшествующих ему middleware
есть элементы cursor
с определенными
значениями prefix
, к который применяется какая-то особая логика.
Декоратор применяется на middleware
-функцию, таким образом, что в момент, когда эта middleware
используется на любом из участков маршрутной карты, маркер применяется к конечной точке согласно правилам
функции маркировки.
Декоратор @Marker()
принимает аргументом функцию маркировки, которая должна принимать два аргумента:
route
и cursor
. Курсором будет всегда прослойка, к которой применен декоратор @Marker
Маркировка устанавливается в процессе сбора маршрутной карты и не оперирует контекстом. Наличие маркировки в элементе маршрута может служить основанием для дополнительных контекстных проверок: полномочий, прав доступа и других составных операций.
Рассмотрим использование маркировки на примере контроля доступа к маршрутным точкам.
// для проверки прав доступа применяется модель данных, которая использует точечное хранение// конечных и промежуточных участков маршрута с указанием ролей, которым данные полномочия разрешены// пользователи могут иметь одну или несколько ролей, которые позволяют ему обращаться к разным методамclass Access { // прослойка, выполняющая проверку, что пользователю, авторизованному в контексте, разрешен // доступ к данному участку маршрута @Middleware() // укажем, что данная прослойка является маркером, использущим указанную функцию маркировки @Marker(Access.setMark) static Check( @StateMap(Auth) { user }: Auth, // авторизационные данные пользователя @Route() route, // точка назначения, из которой важно знать `path` и `method` @Cursor() cursor, // курсор, в котором важно знать значение `prefix` @Next() next, @Err() err ) { // если для пользователя выполняется проверка, то позволим пройти данную прослойку, ведущую к указанному адресу if (user.checkAccess(route, cursor)) { return next(); } else { // иначе вернем ошибку 403 return err("access denied", 403); } }
// создадим имя маркера static markerName = "check_access"; // функция маркировки static setMark(route: IRoute, cursor: ICursor) { const { markerName } = this; // если для элемента `route` нет требуемого маркера, то создадим его if (!route[markerName]) { route[markerName] = []; } // добавим текущий курсор в маркерный список для route route[markerName].push(cursor); }}// ... применим созданный маркер// будем использовать список маршрутов из собранной маршрутной карты, созданной при запуске сервераimport { $aom } from "./server";
@Bridge("/users", Users)class Root { @Get() static Index() { return $aom.routes; }
@Get("/info") // применим middleware, выполняющую функцию маркировки // маркировка распространится на метод `Root.Secure` @Use(Access.Check) static Secure() { return "this route is secure"; }}
// применим middleware, выполняющую функцию маркировки// маркировка распространится на все методы узла Users@Use(Access.Check)class Users { @Get() static Index() { return models.Users.find(); }
@Post("/add") static Add(@Body() body) { return models.Users.create({ ...body }); }
@Delete("/:user_id") @Use(Access.Check) static Delete(@Params() { user_id }) { return models.Users.remove({ _id: user_id }); }}
В результате данной операции в списке маршрутов routes
появятся следующие значения
[ { method: "get", path: "/", }, { method: "get", path: "/secure", check_access: [{ prefix: "/secure" }], }, { method: "get", path: "/users", check_access: [{ prefix: "/users" }], }, { method: "delete", path: "/users/:user_id", check_access: [{ prefix: "/users" }, { prefix: "/users/:user_id" }], }, { method: "post", path: "/users/add", check_access: [{ prefix: "/users" }], },];
Наличие значения check_access
для конечных точек будет являться признаком того, что эти точки
управляются функцией контроля доступа. Таким образом маркировка "подняла наверх" информацию, которую
можно использовать для визуализации структуры запросов и использовании тех из них, к которым
следует применить релевантные маркировке процедуры.
#
Циклические зависимостиaom
подразумевает повторное применение одних классов в контексте других, что может порождать циклические
зависимости модулей. Критически важное значение это имеет в случаях применения декораторов StateMap
и This
,
а также Bridge
и Use
.
Для решения этой проблемы используется функция FwdRef
.
Пример:
// ... users.tsimport { Query, This, Bridge, Get } from "aom";import { User } from "./user";
@Bridge(`/user_${User.id}`, User)class Users { model = getModelForClass(classes.Users); // для контекстного экземпляра создадим модель typegoose вокруг класса `classes.Users`
@Get() static Index(@Query() query, @This() { model }: Users) { return model.find({ ...query }); }}
// ... user.tsimport { Query, This, Bridge, Get, FwdRef } from "aom";
// для eslint-а отключим обработку ошибки циклической зависимости// eslint-disable-next-line import/no-cycleimport { Users } from "./users";
@Use(User.Init)class User { // вместо того, чтобы объявлять для узла собственное значение модели данных // используем его из класса `Users` @Get() static Index(@Query() query, @This(FwdRef(() => Users)) { model }: Users) { return model.find({ ...query }); }}
Если использовать просто @This(Users)
, то в аргументах к декоратору будет передано значение undefined
,
что приведет к получению экземпляра класса User
, и значение model
будет недоступно.
Для других декораторов функция FwdRef
применяется следующим образом:
@Use(FwdRef(()=>Node.Middleware))
@Bridge('/path', FwdRef(()=>NextNode))
@StateMap(FwdRef(()=>AnotherNode)
Важно: настоятельно рекомендуется использовать eslint
с активным правилом import/no-cycle
,
чтобы детектировать ситуации с циклическими ссылками и правильно применять FwdRef