Декораторы аргументов
Все методы, участвующие в маршрутных участках должны использовать декорированные аргументы, чтобы корректно оперировать контекстом действий. Все декораторы возвращают изолированные значения в контексте текущего запроса.
#
ArgsБазовый декоратор @Args
позволяет получить общую структуру данных, являющихся текущим контекстом
выполняемого запроса.
В общем виде эта структура имеет вид:
interface IArgs { ctx: Context; next: Next; route: IRoute; cursor: ICursor;}
Где:
ctx
иnext
- типовые значения, которыми оперируетkoa
route
- структура, указывающая на конечную точку маршрутаcursor
- структура, указывающая на текущую точку маршрута
Остановимся подробнее на cursor
и route
, так как они играют важную роль в организации структур
маршрутов.
Структура cursor
имеет вид:
interface ICursor { constructor: Function; // класс, который в данный момент вызывается property: string; // имя метода, который в данный момент исполняется handler: Function; // собственно функция, которая в данный момент исполняется (handler === constructor[property]) prefix: string; // префикс участка маршрутного пути, который в данный момент проходит курсор}
Структура route
имеет вид:
interface IRoute { constructor: Function; // класс, который содержит конечную точку маршрута property: string; // имя метода, который будет вызван в конечной точке маршрута handler: Function; // собственно функция, которая будет вызвана в конечной точке маршрута (handler === constructor[property]) method: string; // метод, который применяется для конечной точки path: string; // полный путь маршрута (в виде паттерна с параметрами `/files/:filename`) cursors: ICursor[]; // список всех курсоров, составляющих данный маршрут middlewares: Function[]; // список скомплированных функций, запускающихся для данного endpoint в контексте `koa` (функции `(ctx, next)=> {...}`)}
Рассмотрим пример вызова метода GET /users/user_:id
, который в общем случае составлен из цепочки
задекорированных при помощи @Middleware
, @Bridge
и @Endpoint
статичных методов трех классов:
[Root.Init, Users.Init, Users.UserBridge, User.Init, User.Index];
При обращении к данному маршруту будут последовательно вызваны все функции цепочки, и в случае, если
каждая из них корректно вернет next
-значение, будет вызвана конечная функция, в которой ожидается
результат.
На любом из участков маршрута в любой middleware значение route
будет иметь вид:
{ constructor: User, property: `Index`, handler: User.Index, method: "get", path: "/users/user_:id", cursors: [ { constructor: Root, property: "Init", handler: Root.Init, prefix: "/" }, { constructor: Users, property: "Init", handler: Users.Init, prefix: "/users" }, { constructor: Users, property: "UserBridge", handler: Users.UserBridge, prefix: "/users/user_:id", }, { constructor: User, property: "Init", handler: User.Init, prefix: "/users/user_:id" }, { constructor: User, property: "Index", handler: User.Index, prefix: "/users/user_:id" }, ], middlewares: [async (ctx, next)=> {...}, ....]};
Таким образом в любом месте маршрута можно получить информацию о точке назначения, и при необходимости выполнить какие-либо проверки или залогировать действия.
Значение cursor
в каждом месте маршрута будет отличаться.
Для первого элемента он будет равен:
{ constructor: Root, property: `Init`, handler: Root.Init, prefix: '/'}
Для второго элемента он будет равен:
{ constructor: Users, property: `Init`, handler: Users.Init, prefix: '/users'}
Для третьего:
{ constructor: Users, property: `UserBridge`, handler: Users.UserBridge, prefix: '/users/user_:id'}
Для четвертого:
{ constructor: User, property: `Init`, handler: User.Init, prefix: '/users/user_:id'}
Для пятого:
{ constructor: User, property: `Index`, handler: User.Index, prefix: '/users/user_:id'}
Таким образом на каждом шаге маршрута может быть получена рефлексивная информация о том, кто и на каком участке его обрабатывает. Может быть использовано для логирования, контроля доступа к маршрутам, а также к сохранению и применению контекстных данных на любом из его участков.
Если применялись составные маршруты с применением декоратора @UseNext
, то последующие вызываемые функции
будут включены в общий список курсоров, и будут идти после значения, определяющего саму конечную точку, и
иметь то же значение prefix
.
Наличие в route
и cursor
значения constructor
дает возможность использовать значения из структуры
ctx.$StateMap = new WeakMap
, которые более подробно рассматриваются в описании к декораторам
StateMap
и This
.
Значения объекта route
одинаково для всех точек на ветке маршруте. Значения в структуре route
могут быть расширены за счет декоратора @Marker
(описан ниже)
Для объекта cursor
значение constructor
может быть изменено в особом случае: если применяется
декоратор перегрузки Sticker
(описан ниже)
Декоратор Args
позволяет принять на вход функцию, которой будет передана структура аргументов IArgs
,
из которых могут быть извлечены и возвращены специфические значения. Допускается применение асинхронных функций.
Пример:
import { Args, Get } from "aom";const getUrl = (args) => args.ctx.url;class Index { @Get() static Main(@Args(getUrl) url) { return url; }}
Допускается создание собственных декораторов аргументов, используя вызов Args
import { Args, Get } from "aom";const Url = () => { const handler = (args) => args.ctx.url; return Args(handler);};class Index { @Get() static Main(@Url() url) { return url; }}
Все существующие декораторы аргументов являются частными случаями применения декоратора @Args
:
#
CtxДекоратор @Ctx()
- возвращает стандартный для koa
объект ctx
, к которому могут быть применены
его типовые методы, извлечены стандартные или, если использовались специфические библиотеки,
особые значения.
#
Req, ResДекораторы @Req()
и @Res()
возвращают стандартные для koa
объекты ctx.req
и ctx.res
соответственно. Не принимают никаких аргументов, позволяют на низком уровне работать с контекстом.
#
NextДекоратор @Next()
позволяет получить специальную next
-функцию.
В общем случае next
-функция используется аналогично стандартной next
-функции koa
: указывает,
что далее ожидается результат из следующей функции в цепочке. Чаще всего применяется в качестве
значения, возвращаемого в middleware
.
При использовании аргументов next
-функция позволяет вернуть результат из другого endpoint
или middleware
. В качестве аргументов принимает последовательность статичных методов, являющихся
точкой назначения или прослойкой.
Пример:
@Use(User.Init)class User { data: any;
@Middleware() static async Init(@Params("user_id") userId, @This() user: User, @Next() next) { user.data = await models.Users.findById(userId); return next(); // при вызове без аргументов указывает, что ожидается следующая функция в цепочке }
@Get() static Info(@This() { data }: User) { return data; }
@Patch() static async Update(@This() { data }: User, @Body() body, @Next() next) { const { _id } = data; await models.Users.update({ _id }, { $set: body }); // может принимать в качестве аргументов цепочку middleware и endpoint // выполняет их последовательно и возвращает результат, соответствующий последнему значению в цепочке // прерывает выполнение в случае ошибки return next(User.Init, User.Info); }}
#
ErrДекоратор @Err()
возвращает error
-функцию. В общем случае aom
отреагирует на throw
в произвольном
месте цепочки вызовов, и вернет ее как 500 ошибку (или использует значение status
из объекта ошибки).
error
-функция, полученная декоратором @Err
позволит вернуть ошибку с указанным кодом status
и дополнительной информацией data
.
Декоратор может принимать аргументом конструктор ошибки, который будет создан при генерации ошибки.
Важно: конструктор ошибки должен быть унаследован от класса Error
.
Создаваемая error
-функция при вызове использует аргументы:
- message: string - сообщение об ошибке, обязательно
- status?: number - код ошибки, по умолчанию 500
- data?: any - произвольная структура с данными об ошибке
Функцию можно вернуть через return
или throw
.
Пример:
import { Params, Err, Next, Middleware } from "aom";
class ErrorResponse extends Error { status: number; data: any; constructor(message, status = 500, data = undefined) { this.message = message; this.status = status; this.data = data; }
static toJSON() { return { message: this.message, status: this.status, data: this.data }; }}
@Use(User.Init)class User { @Middleware() static async Init(@Params("user_id") userId, @Err(ErrorResponse) err, @Next() next) { const user = await models.Users.findById(userId); if (user) { return next(); } else { // вернет ошибку с кодом 404 и сообщением "user not found" // в качестве data будет значение объект с параметром, не прошедшим проверку // будет создан экземпляр класса ErrorReponse return err("user not found", 404, { user_id: userId }); } } // или @Middleware() static async Init(@Params("user_id") userId, @Err() err, @Next() next) { const user = await models.Users.findById(userId); if (user) { return next(); } else { // вернет ошибку с кодом 404 и сообщением "user not found", в качестве data будет значение // в качестве ошибки будет экземпляр класса Error return err("user not found", 404, { user_id: userId }); } }}
#
Другие способы перехвата ошибокВызов задекорированных методов в aom
происходит внутри конструкции try { } catch (e) { }
: таким образом
любой throw
будет интерпретирован как ошибка на маршруте, даже если был вызван сторонней библиотекой,
и будет возвращен в качестве значения ctx.body = e
, прервав выполнение маршрута.
Вместо вызова error
-функции также можно возвращать экземпляр ошибки: aom
проверяет, если
возвращаемое значение является объектом ошибки, то прекратит выполнение маршрута, и вернет ошибку
с кодом 500, или со значением status
, если таковое присутствует в значении.
Таким образом, вместо error
-функции можно использовать собственный тип ошибок, который унаследован
от класса Error
.
Например:
// ... используем класс ErrorResponse, описанный вышеclass Auth { @Middleware() static Required(@Next() next, @Headers("authorization") token) { if (await models.Auth.checkToken(token)) { return next(); } else { return new ErrorResponse("access denied", 403); } }}
#
QueryДекоратор @Query()
позволяет получить значение ctx.query
, типичное для koa
.
import { Get, Query } from "aom";import fs from "fs";
class Files { @Get() static Index(@Query() query) { const { name } = query; return fs .readdirSync(__dirname) .filter((filename) => (name ? filename.search(name) >= 0 : true)); }}
Декоратор может принимать в качестве аргумента функцию-обработчик, в которой можно преобразовать или проверять значения входящего объекта.
const QueryParser = (query) => { const { offset = 0, limit = 10, sort = "name", ...where } = query; return { offset, limit, sort, where };};
class Users { @Get("/search") static Search(@Query(QueryParser) { where, offset, sort, limit }) { return models.Users.find(where).order(sort).offset(offset).limit(limit); }}
#
BodyДекоратор @Body()
позволяет получить значение ctx.request.body
, типичное для koa
.
import { Get, Body } from "aom";import fs from "fs";
class Users { @Post() static save(@Body() body) { return models.Users.create(body); }}
Декоратор может принимать в качестве аргумента функцию-обработчик, в которой можно преобразовать или проверять значения входящего объекта.
// использутся `class-transformer` и `class-validator`, подразумевая, что в модели данных// применяются соответствующие декораторыimport { plainToClass } from "class-transformer";import { validate } from "class-validator";// разрешается использовать асинхронные функцииconst ValidateBody = async (body) => { const safeBody = plainToClass(models.Users, { ...body }); const validateErrors = await validate(safeBody, { whitelist: true });
if (validateErrors.length) { throw Object.assign(new Error("validation error"), { data: validateErrors }); } return safeBody;};
class Users { @Post("/add") static Add(@Body(ValidateBody) userData) { // в `userData` заведомо точно будут безопасные данные, которые можно добавлять в базу return models.Users.create({ ...userData }); }}
#
ParamsДекоратор @Params()
позволяет получить значения ctx.params
, типичное для koa
. Может принимать
в качестве аргумента имя параметра, возвращая его значение.
import { Get, Middleware, Params, Next } from "aom";
class User { @Middleware() static async Init(@Params() params, @Next() next) { const user = await models.Users.findById(params.user_id); return next(); } // или @Middleware() static async Init(@Params("user_id") userId, @Next() next) { const user = await models.Users.findById(userId); return next(); }}
#
HeadersДекоратор @Headers()
позволяет получить значения ctx.headers
, типичное для koa
. Может принимать
в качестве аргумента имя заголовка, возвращая его значение.
import { Get, Headers, Middleware, Next } from "aom";
class Auth { @Middleware() static async Init(@Headers() headers, @Next() next) { const checkToken = await models.Auth.checkToken(headers.authorization); return next(); } // или @Middleware() static async Init(@Headers("authorization") authToken, @Next() next) { const checkToken = await models.Auth.checkToken(authToken); return next(); }}
#
StateДекоратор @State()
позволяет получить значения ctx.state
, типичное для koa
. Может принимать
в качестве аргумента имя аттрибута, возвращая его значение.
import { Get, State, Params, Middleware, Next } from "aom";
@Use(User.Init)class User { // сохраним значение в state @Middleware() static async Init(@State() state, @Params("user_id") userId, @Next() next) { state.user = await models.Users.findById(userId); return next(); }
// извлечем значение из state @Get() static async Index(@State("user") user) { return user; }}
#
SessionДекоратор @Session()
позволяет получить значения ctx.session
, типичное для koa
. Может принимать
в качестве аргумента имя аттрибута, возвращая его значение.
Важно: необходимо использовать middleware-библиотеки для использования сессий в koa
(например: koa-session
)
Пример:
import { Middleware, Post, Delete, Session, Body } from "aom";
@Use(Basket.Init)class Basket { // убедимся, что есть список для хранения товаров в корзине @Middleware() static Init(@Session() session, @Next() next) { if (!session.basket) { session.basket = []; } return next(); } // добавим предмет в корзину @Post() static async AddItem(@Body() item, @Session("basket") basket) { basket.push(item); return basket; }
// очистим корзину @Delete() static async Clear(@Session() session) { session.basket = []; return basket; }}
#
FilesДекоратор @Files()
позволяет получить данные из ctx.request.files
, типичного для большинства
библиотек koa
, позволяющего загружать файлы.
Важно: необходимо использовать middleware-библиотеки для загрузки файлов в koa
(например: koa-body
)
import { Post, Files } from "aom";import fs from "fs";import path from "path";
class Files { // загрузка одного файла @Post() static UploadFiles(@Files("file") file: File) { const filename = path.join(__dirname, file.name); fs.renameSync(file.path, filename); return file; } // загрузка нескольких файлов @Post("/mass_upload") static UploadFiles(@Files() files: Record<string, File>) { const filenames = []; Object.keys(files).forEach((key) => { const file = files[key]; const filename = path.join(__dirname, file.name); fs.renameSync(file.path, filename); filenames.push(filename); }); return filenames; }}
#
CursorДекоратор @Cursor()
позволяет получить значение cursor
, описанное выше.
#
RouteДекоратор @Route()
позволяет получить значение route
, описанное выше.
#
StateMapaom
расширяет контекстное значение koa
специальной конструкцией ctx.$StateMap = new WeakMap()
, которое
позволяет сохранять в контексте связи, основанные на ассоциациях с абстрактными ключами. В частности
для aom
это позволяет сохранять ассоциации на классах, образующих маршрутные узлы.
Наиболее частый способ применения @StateMap()
- сохранение в middleware
-функции локальные состояния
экземпляров класса с последующим их применением в других методах.
Декоратор StateMap
может принимать аргумент, который вернет из хранилища значение по ключу, равному этому
аргументу.
Пример:
class Auth { user: models.Users; login: models.UserLogins; // создадим прослойку, которая по токену определяет, доступна ли пользователю авторизация // и если доступна, сохраняет в stateMap по ключу класса авторизационную информацию: пользователя и логин @Middleware() static Init(@Headers("authorization") token, @Next() next, @StateMap() stateMap, @Err() err) { const authData = models.Auth.checkToken(token); if (authData) { const auth = new this(); // поскольку метод вызывается с сохранением контекста, то `this` - это класс `Auth` auth.user = await models.Users.findById(authData.userId); auth.login = await models.UserLogins.findById(authData.userLoginId); stateMap.set(this, auth); } else { return err("wrong auth", 403, { token }); } }}// ... затем извлечем доступ к авторизационной информации в другом middleware или endpoint
@Use(Auth.Init) // отметим, что для доступа к маршрутному узлу обязательна успешная авторизацияclass Account { // этот метод будет гарантированно вызван, если авторизация по токену была успешно совершена // а значит в StateMap будет значение по ключу Auth, являющееся экземпляром этого класса // с установленными значеними @Get() static async Index(@StateMap(Auth) auth: Auth, @Next() next) { const { user, login } = auth; // поскольку user - это объект модели данных `models.Users`, то для него доступны все его методы const stat = await user.getStat(); return { user, login, stat }; }}
Использование WeakMap
обусловленно критериями скорости и оптимизации памяти для хранения значений.
При желании его можно перегрузить, создав middleware
, в котором будет использовано хранилище Map
.
Например:
@Use(Root.Init) // Root.Init будет вызываться перед всеми запросами во всех маршрутных ветках@Bridge("/files", Files)@Bridge("/users", Users)class Root { @Middleware() static Init(@Ctx() ctx, @Next() next) { ctx.$StateMap = new Map(); return next(); }
@Get() static Index() { return "index page"; }}
#
ThisДекоратор @This()
является расширением декоратора @StateMap()
. Он проверяет, есть ли в ctx.$StateMap
значение по ключу, равного значению constructor
в текущем cursor
. Таким образом в общем случае он
проверяет, есть ли в StateMap
значение для текущего класса, который сейчас выполняет работу, и если нет,
создает его singletone-экземпляр и возвращает значение.
Наиболее частый способ применения декоратора @This()
- использование в иницирующей middleware
и endpoint
-ах одного и того же маршрутного узла.
@Use(User.Init)class User { user: models.Users; stat: any;
@Middleware() static async Init(@Params() { user_id }, @Next() next, @Err() err, @This() _this: User) { const userInfo = await models.Users.findById(user_id); if (userInfo) { _this.user = userInfo; _this.stat = await userInfo.getStat(); return next(); } else { return err("user not found", 404); } }
@Get() static Info(@This() user: User) { return user; // вернет { user, stat } }
@Delete() static async Delete(@This() { user }: User) { const result = await user.delete(); return result; }}
Декоратор @This()
может принимать в качестве аргумента, другой класс. В этом случае будет возвращено
значение для этого класса из ctx.$StateMap
, а, если его там не было, будет создан и возвращен экземпляр
этого класса, с сохранением по указанному аргументу в ctx.$StateMap
.
class Files { where = {};
@Get() static Index(@This() { where }: Files) { return models.Files.find({ ...where }); }}
// ...class User { user: models.Users;
@Bridge("/files", Files) static userFiles(@This() { user }: User, @This(Files) files: Files, @Next() next) { files.where = { userId: user.id }; return next(); }}
Таким образом, использование декоратора @StateMap()
позволяет хранить по ключу произвольное значение,
в то время как @This()
всегда возвращает singletone-экземпляр класса, переданного в аргументе или
в текущем курсоре.
Важно: все классы, для которых будет применяться декоратор @This
, должны уметь создавать свои
экземпляры без аргументов, так как декоратор не поддерживает передачу каких-либо значений в конструктор.