Перейти к основному содержимому

Декораторы аргументов

Все методы, участвующие в маршрутных участках должны использовать декорированные аргументы, чтобы корректно оперировать контекстом действий. Все декораторы возвращают изолированные значения в контексте текущего запроса.

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, описанное выше.

StateMap#

aom расширяет контекстное значение 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, должны уметь создавать свои экземпляры без аргументов, так как декоратор не поддерживает передачу каких-либо значений в конструктор.