1.1.2 • Published 7 months ago

@tanhuy998/context-js v1.1.2

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

Context-JS

Provide abstraction on controller base programming with Express inspired by ASP.NET and Java Spring and Laravel.

Features

  • Websocket controller (beta since v1.1.0)
  • Handling request by using controller classes.
  • Routing with controller.
  • Dependency Injection.

Acknowledgment

This project codebase is implementing coding convention of legacy version of @babel/plugin-decorator-proposal which is implementing stage 1 tc39/proposal-decorators. In case using this package with Typescript, we recommend using @babel/preset-typescript as transpiller instead of tsc because this package is not compatible with Typescript experimental decorator.

Usage

Installation

npm:

npm install @tanhuy998/context-js

yarn:

yarn add @tanhuy998/context-js

Configuration

Merge one of the following methods with you babel configuration.

.babelrc or babel.config.json

{
  "presets": [
        "@babel/preset-env"
    ],
  "plugins": [["@babel/plugin-proposal-decorators", { "version": "legacy" }]]
}

package.json

{
  "babel" : {
    "presets": [
        "@babel/preset-env"
    ],
    "plugins": [["@babel/plugin-proposal-decorators", { "version": "legacy" }]]
  }
}

*update

To use decorator with class's private properties. Change the version of @babel/plugin-decorator-proposal to "2022-03". Stage 3 of TC39-proposal-decorator supports applying decorator on private property that is not supported on Stage 1.

The config will be

{
  "plugins": [["@babel/plugin-proposal-decorators", { "version": "2022-03" }]]
}

Note

The whole package does not use Decorators internally. The file which use decorator syntax must be transpiled before execution.

require('@babel/register')({
    only: [
        'Path/to/The/Controller/File',
    ]
}); // to tell Babel that the file needs to be tranpiled 


const app = require('express')();
const {dispatchRequest} = require('@tanhuy998/context-js');

const Controller = require('Path/to/The/Controller/File');

app.get('/path', dispatchRequest(...Controller.action.sendSomething));

or you can transpile files before import it. This reduce runtime latency (spend on transpilling the syntax).

This inconvience will no longer happens when Decorator feature officially been released.

Simple usage

Define a controller class

const {BaseController, dispatchable} = require('@tanhuy998/context-js');

@dispatchable // => (optional)
class Controller extends BaseController {

    construct() {

      super();
    }

    sendSomething() {

      const res = this.httpContext.response;

      res.send('Hello World');
    }
}

Import into Express:

require('@babel/register')({
    only: [
        'Path/to/The/Controller/File',
    ]
}); 

const app = require('express')();
const {dispatchRequest} = require('@tanhuy998/context-js');

const Controller = require('Path/to/The/Controller/File');

app.get('/path', dispatchRequest(...Controller.action.sendSomething));

or (without @dispatchable decorator)

// when not using decorator @dispatchable on Controller
app.get('/path', dispatchRequest(Controller, 'sendSonthing'));

Go to the detail

A controller class which can be assigned to a route must extends BaseController class. Then, the controller will be dispatched a httpContext when a request arrive.

To access to the context, use this.httpContext (borrowing the concept of ASP.NET) inside *controllers.

The httpContext is an instance of HttpContext class that has regular properties

{
    request: req,               // the request object dispatched from express
    response: res,              // the response object dispatched from express
    nextMiddleware: next,       // the function that point to the next handler of the current route
}

Routing with Controllers

Firstly hit the codes:

const express = require('express');
const {ApplicationContext} = require('@tanhuy998/context-js');

// Controller files must be imported before ApplicationContext resolves routes
const Controller = require('The/Controller/directory');

const app = express();

app.use('/', ApplicationContext.resolveRoutes());

in class definition:

Apply @routingContext() on class to annotate that the specific class is defining route inside

const {Route, BaseController, dispatchable, routingContext} = require('@tanhuy998/context-js');

/*
* use @routingContext() decorator to annotate that the controller
* is mapping routes inside.
*/
@routingContext()
class Controller extends BaseController {

    constructor() {

      super();
    }
    
    @Route.get('/send')  // define an enpoint
    sendSomthing() {


    }
}

DO NOT miss the @routingContext() in each class definition when routing, it would cause unexpected routes mapping.

@routingContext()
class FirstController extends BaseController {

    constructor() {

      super();
    }

    @Route.get('/send')
    sendSomthing() {


    }

    index() {

      conosle.log('This is the "index" method of Controller class');
    }
}

class SecondController extends BaseController {

  contructor() {

    super();
  }

  @Route.get('/index') 
  index() {

    console.log('What we want to see');
  }
}

When hit a request to the enpoint

GET /index

the output on the console will print

This is the "index" method of Controller class

It happens because the SecondController has no routing context, then the @Route.get('/index') mapping on SecondController class refers to the latest routing context (FirstController). So, the endpoint GET /index will be mapped to FirstController.index() instead of SecondController.index(). In case if the FirstController class did not define the index() method, requesting to GET /index would cause FirstController.index is not a funtion error.

Routes prefix

syntax:

@Route.prefix('/example')
class Controller extends BaseController {}
const {Route, BaseController, routingContext} = require('@tanhuy998/context-js');

/*
* Route.prefix decorator affect on class.
* all the routes which is declared inside that class will be concatenated
* with the prefix
* if there is more than one is called,
* the last call of Route.prefix will affects fo
*/
@Route.prefix('/admin')
@routingContext()  // routingContext() must be placed before Route.prefix()
class Controller extends BaseController {

    constructor() {

      super();
    }

    // the route path is 'GET /user/send'
    @Route.get('/send')
    sendSomthing() {


    }
}

when calling multiple prefix on controller

const {Route, BaseController, dispatchable, routingContext} = require('@tanhuy998/context-js');

@Route.prefix('/multiple')
@Route.prefix('/single')
@routingContext()
class Controller extends BaseController {

    constructor() {

      super();
    }

    // the enpoint's path will be 'get /multiple/send'
    // the '/single' prefix is ovetwritten
    @Route.get('/send')
    sendSomthing() {


    }
}

Middleware

Apply @Middleware on endpoint to register middlewares to that endpoint.

Syntax:

// Invokes before the controller's action.
@Middleware.before(...theMiddlewareIntances)

// invokes after the controller's action done.
@Middleware.after(...theMiddlewareIntances)

example

const {Route, BaseController, Middleware} = require('@tanhuy998/context-js');

const bodyParser = require('body-parser');

function log(req, res, next()) {

  console.log(req.method, req.baseUrl + req.path);
  next();
}

@routingContext()
class Controller extends BaseController {

    constructor() {

      super();
    }

    @Route.post('/post')
    @Middleware.before(bodyParser.json())
    receiveSomething() {

      const req_body = this.httpContext.request.body;

    }

    @Route.get('/get')
    @Middleware.after(log)
    doSomthing() {

      // *beware that when setting middleware after a controller's action
      // we must call next() to delegate next middleware
      this.httpContext.nextMiddleware();
    }
}

*Note: When a controller's action has no endpoint defined on it, all middleware applied to that action is meaningless.

Best practice

Using @Middleware like the follwing example is not be adviced, it will be confusing for reading.

@routingContext()
class Controller extends BaseController {

    constructor() {

      super();
    }

    @Route.post('/post')                             
    @Middleware.before(func1, func2)                    
    @Middleware.before(func3, func4, func5)          
    mistake1() {

      // the flow of route invocation will be
      // func3 -> func4 -> func5 -> func1 -> func2 -> Controller's Action   
  
    }

    @Route.get('/get')                             
    @Middleware.after(func1, func2)                    
    @Middleware.after(func3, func4, func5)
    mistake2() {

      // the flow of route invocation will be
      // Controller's Action -> func3 -> func4 -> func5 -> func1 -> func2

      this.httpContext.nextMiddleware();
    }
}

You are recommended to use single @Middleware for all middleware functions like

@Middleware.before(func1, func2, func3, func4, func4)
Route.get('/get')  
controllerMethod() {

}

or single @Middleware for a single middleware function on multiple times

@Middleware.before(func5)
@Middleware.before(func4)
@Middleware.before(func3)
@Middleware.before(func2)
@Middleware.before(func1)
Route.get('/get')  
controllerMethod() {

 // func1 -> func2 -> func3 -> func4 -> func5 -> Controller's Action   
}

Group

Group is a way to apply particular constraint (Middlewares) on a set of endpoints.

const {Route, BaseController, routingContext} = require('@tanhuy998/context-js');


@Route.group('/user')
@routingContext()
class Controller extends BaseController {

    constructor() {

      super();
    }

    // the path for of the route will be GET /user/send
    @Route.get('/send')
    sendSomthing() {


    }
}

@Route.group vs @Route.prefix

@Route.prefix is just the concaternation of the prefix and the endpoint's path. The latest prefix is the one accepted to the controller. On the other hand, group can be various on a single controller.

Group Local Contraint

Local constraint is middlewares that is registered to all endpoint what is declared inside a controller. To add local constraint to a group, apply @Middleware on controller.

const {Route, BaseController, routingContext} = require('@tanhuy998/context-js');
    
function log(req, res, next) {
  console.log(req);
  next()
}


@Route.group('/user')
@Middleware.before(log)
@routingContext()
class UserController extends BaseController {

    constructor() {

      super();
    }


    @Route.get('/send')
    sendSomthing() {


    }
}

Default group

If using @Middleware without declaring any groups immediately, the specific controller is declared with group '/' by default.

const {Route, BaseController, routingContext} = require('@tanhuy998/context-js');
    
function log(req, res, next) {

  console.log(req);
  next()
}


@Middleware.before(log)
@routingContext()
class UserController extends BaseController {

    constructor() {

      super();
    }

    @Route.get('/send')
    sendSomthing() {


    }
}

Groups's local constraint Isolation

Groups in different controllers (routing context) are isolated, so their local constraints are different no matter how whose path are identical. The following example shows how groups '/v1' is isolated on each Controller.

const {Route, BaseController, routingContext} = require('@tanhuy998/context-js');
    

function userLog() {

  console.log('user log')
}

function adminLog() {

  console.log('admin log');
}

@Route.group('/v1')
@Middleware.after(userLog)
@routingContext()
class UserController extends BaseController {

    constructor() {

      super();
    }


    @Route.get('/send')
    sendSomthing() {

        this.httpContext.nextMiddleware();
    }
}

@Middleware.after(adminLog)
@Route.group('/v1')
@routingContext()
class AdminController extends BaseController {

    constructor() {

      super();
    }


    @Route.get('/index')
    index() {

        this.httpContext.nextMiddlware();
    }
}

Group Global Contraint

Global Constraint is opposite to Local Constraint. Groups that have same path will inherit global constraint that is managed on them.

const {Route, BaseController, routingContext} = require('@tanhuy998/context-js');

// define global constraint for groups
Route.constraint()  // the purpose of this global constraint is to meassure the time the request is handled.
      .group('/meassure')
      .before(start) // invoke before the handlers chain of it's route
      .after(end) // invoke after the handlers chain of it's route
      .apply()

function start(req, res, next) {

  req.startTime = Date.now();
  next();
}

function end(req, res, next) {

  const now = Date.now();
  const start = req.startTime;

  console.log('the time for the request is', now - start);
}


@Route.group('/user')
@Route.group('/meassure')
@routingContext()
class UserController extends BaseController {

    constructor() {

      super();
    }

    @Route.get('/send')
    sendSomthing() {

        this.httpContext.nextMiddleware();
    }
}


@Route.group('/admin')
@Route.group('/meassure')
@routingContext()
class AdminController extends BaseController {

    constructor() {

      super();
    }

    @Route.get('/index')
    sendSomthing() {

        this.httpContext.nextMiddleware();
    }
}

Desugar groups

Context.JS maps route base on Express.js's router. Consider the previous example

const {Route, BaseController, routingContext, Middleware} = require('@tanhuy998/context-js');


function start(req, res, next) {

  req.startTime = Date.now();
  next();
}

function end(req, res, next) {

  const now = Date.now();
  const start = req.startTime;

  console.log('the time for the request is', now - start);
}

function userLog(req, res, next) {

  console.log('user log')

  next();
}

function adminLog(req, res, next) {

  console.log('admin log');

  next();
}

Route.constraint()
      .group('/messure')
      .before(start) // invoke before the handlers chain of it's route
      .after(end) // invoke after the handlers chain of it's route
      .apply()


@Route.group('/user')
@Route.group('/messure')
@Middleware.after(userLog)
@routingContext()
class UserController extends BaseController {

    constructor() {

      super();
    }

    @Route.get('/send')
    sendSomthing() {


    }
}


@Route.group('/admin')
@Route.group('/messure')
@Middleware.after(adminLog)
@routingContext()
class AdminController extends BaseController {

    constructor() {

      super();
    }

    @Route.get('/index')
    sendSomthing() {


    }
}

then Context-JS explains the related decorators to Express app how to map the routes as following.

const express = require('express');
const app = express();
const {dispatchRequest} = require('contextjs');

const mainRouter = express.Router();

const userEndpoints = express.Router();
userEndpoints.get('/send', dispatchRequest(UserController, 'sendSomething'), userLog);

const adminEndpoints = express.Router();
adminEndpoints.get('/index',dispatchRequest(AdminController, 'sendSomething'), adminLog)


const messureGroup = express.Router();
messureGroup.use(userEndpoint);
messureGroup.use(adminEndpoint);


mainRouter.use('/user', userEndpoints);
mainRouter.use('/admin', adminEndpoints);
mainRouter.use('/messure', start, messureGroup, end);

app.use('/', mainRouter);


function start(req, res, next) {

  req.startTime = Date.now();
  next();
}

function end(req, res, next) {

  const now = Date.now();
  const start = req.startTime;

  console.log('the time for the request is', now - start);
}

function userLog(req, res, next) {

  console.log('user log')

  next();
}

function adminLog(req, res, next) {

  console.log('admin log');

  next();
}

Priority of constraints

The flow of Constraints is described below.

[globalConstraint.before] 
-> [localConstraint.before] 
-> [endpointMiddleware.before] 
-> [Controller.action] 
-> [endpointMiddleware.after] 
-> [localConstraint.after] 
-> [globalConstraint.after]

Dependency Injection

This package provides Dependency Injection by using an ioc container to help Javascript users on wiring dependencies across components. Dependency Injection in this package just support two types of injection is Constructor Injection and Property Injection. Borrowing the concept Dependency Injection of ASP.NET and Larvel's Service Container, Components has it's own lifecycle depends on which type that a particular component is bound.

Binding components

Binding components is the way implementing the Dependency Inversion. Binding means telling the Ioc Container which component (concrete) should be injected as abstraction (base classes or interfaces).

There are three types of binding components (also called as component's lifecycle)

  • Sington: The Ioc Container just resolve the component once and then reuse it over the Node global module.
  • Scope: Like Singleton. But the component is resolved when a httpContext arrive. The component will be reuse if the context need it has controller state.
  • Transient: Each time we need a component, the ioc container will build a new intance.
const {ApplicationContext} = require('express-controller-js');

ApplicationContext.useIoc();

// bind singleton
ApplicationContext.components.bindSingleton(abstract, concrete);

// bind scope
ApplicationContext.components.bindScope(abstract, concrete);

// bind transient
ApplicationContext.components.bind(abstract, concrete);

// abstract and concrete are classes or functions. The concrete must inherits the abstract.

Autobinding

Autobinding is the way class binds itself as abastract and concreate

const {autoBind, BindType} = require('express-controller-js');

@autoBind()  // bind itself as Transient component
class Transient {

}

@autoBind(BindType.SCOPE) // bind itself as Scope component
class Scope {
    
}

@autoBind(BindType.SINGLETON) // bind itself as Singleton component
class Singleton {
    
}

Injecting dependencies

Constructor Injection (Method Injection)

Assign the constructor's (method's) parameters default value as the component to annotate the Ioc Container the type of component you want to inject.

const {autoBind, BaseController} = require('@tanhuy998/context-js')

@autoBind()
class ComponentA {}

@autoBind()
class Controller extends BaseController {
    
    prop;
    
    // inject a instance of ComponentA to the parameter
  constructor(component = ComponentA) {
    super();
    
    this.prop = component;
  }
}

Inject components to endpoint's handler

const {autoBind, BaseController, routingContext, Endpoint} = require('@tanhuy998/context-js')

@autoBind()
class ComponentA {

  message = 'Hello World';
}

@routingContext()
@autoBind()
class Controller extends BaseController {
    
    prop;
    
    // inject a instance of ComponentA to the parameter
  constructor(component = ComponentA) {
    super();
    
    this.prop = component;
  }

  @Endpoint.GET('/')
  index(component = ComponentA) {

    console.log(component.message);
  }
}

Note: The ioc container could not inject components to async handler because Babel wraps async method in another sync method that the ioc container could not read reflection to inject components correctly. Anyway, we have an alternative way to inject components to async method as the following.

const {autoBind, BaseController, routingContext, Endpoint, args} = require('@tanhuy998/context-js')

@autoBind()
class ComponentA {

  message = 'Hello World';
}

@routingContext()
@autoBind()
class Controller extends BaseController {
    
    prop;
    
    // inject a instance of ComponentA to the parameter
  constructor(component = ComponentA) {
    super();
    
    this.prop = component;
  }

  @Endpoint.GET('/')
  @args(ComponentA)  // @args also effects on dependency injection
  async asyncIndex(component) {
    /**
     *  Decorator @args pass argumments to the method
     *  ioc container will lookup for possible components on @args
     */
    console.log(component.message);
  }
}

Property injection

Applying @is(Component) to the class annotated with @autobind to inject the exact component. The component which is injected must be register to the ioc container as a bound component.

const {autoBind, is, BaseController} = require('@tanhuy998/context-js')

@autoBind()
class ComponentA {}

@autoBind()
class Controller extends BaseController {

  @is(ComponentA)
  prop;

  constructor() {

    super();
  }
}

Response decorator

The @Response decorator represent the current httpContext.response of current http context. We can invoke some methods of the "response" object.

syntax:

@Response.Method(...args)

example

const {Route, BaseController, dispatchable, Response, routingContext} = require('@tanhuy998/context-js');

@routingContext()
class Controller extends BaseController {

    constructor() {

      super();
    }

    // Beware of using the method of Node.js native Response
    // because it works directly with STREAM and there is no buffering mechanism
    // for http operations.
    // In this example, the response status code and response header will be sent directly to the network
    @Response.writeHead(200, {
      'Content-Type': 'text/plain',
    })
    @Route.get('/index')
    sendSomthing() {

      // this.httpContext.response.status(404) will throw an Error
    }
}

Controller that return response's body

We can annotate a controller to return the response body

Syntax:

@responseBody

Note: using this decorator means that we will calling the res.send(_content) and res.end(), so the response session will end there but Middlewares after the controller action still invoke.

Example

const {BaseController, dispatchable, responseBody} = require('@tanhuy998/context-js');

@dispatchable
class Controller extends BaseController {

    constructor() {

      super();
    }

    
    @responseBody
    sendSomthing() {

      // equilavent to 
      // this.httpContext.response.send('Hello World!');
      // this.httpContext.response.end();

       return 'Hello World!';
    }
}

@responseBody ActionResult

@responseBody deals with IActionResult returned by the controller's action. This package provide 4 types of ActionResult. Action result use the parameter template of Express's response object. Therefore, the following ActionResult use parameter list like standard Express's response parameter template.

const {BaseController, dispatchable, responseBody, view, redirect, file, download} = require('@tanhuy998/context-js');

@dispatchable
class Controller extends BaseController {

    constructor() {

      super();
    }

    
    @responseBody
    view() {
        const data = {
            message: 'Hello world';
        }
      
      /**
       *  response a view to the client
       *  view() depends on the template engine which we register to Express instance
       */
       return view('index', data);
    }
    
    @responseBody
    file() {
        
        /**
         *  response a file to the client
         */ 
        return file('pathToFile');
    }
    
    @responseBody
    redirect() {
        
        /**
         *  rediect to a specified destination
         */ 
        return redirect('url/path');
    }
    
    @responseBody
    download() {
        
        /**
         *  response a file download to the client
         */
        return download('filePath');
    }
}

ActionResult methods

ActionResult.status(code) Set the http status code of the response. An alias of Express.Response.status()

ActionResult.header(field, [value]) Set headers to the response. An alias of Express.Response.header()

ActionResult.cookie(name, value [, options]) Set cookies to the response. An alias of Express.Response.cookie()

ActionResult.clearCookie(name [, options]) clear cookies of the response. An alias of Express.Response.clearCookie()

Example

@responseBody
someMethod() {
    
    return view('index')
            .cookie('name', 'tobi', { domain: '.example.com', path: '/admin', secure: true })
            .header({
                Keep-Alive: 'timeout=5, max=997'
            });
}

Websocket Controller

Since v1.1, Context-js support websocket controller that is compatible with Socket.io package. It requires Socket.io version 4.x or higher on both client and server side.

Websocket features have been built under @babel/plugin-proposal-decorators version 2022-03 so that if legacy version was setted, the websocket features would not work and cause errors.

const {WS, WSController} = require('@tanhuy998/context-js');

@WS.context()
class Controller extends WSController {

    constructor() {

      super();
    }
}

in index.js

const http = require('http');
const {Server} = require('socket.io');
const httpServer = http.createServer();
const io = new Server(httpServer);
const {WS} = require('@tanhuy998/context-js');

// import the controller class immediately
const Controller = require('./path/to/file');

WS.server(io);
WS.resolve()

Handling websocket events

Context-js considers websocket events as channels. It defines a routing mechanism like Express.Router. When an event occurs, the ws router will dispatches the event to a specific handler that is define before.

const {WS, WSController} = require('@tanhuy998/context-js');

@WS.context()
class Controller extends WSController {

    constructor() {

      super();
    }

    @WS.channel('message')
    handleMessage() {

        // return means sending back acknowledgment if the client 
        // await for that
        return {status: 'ok'};
    }
}

Emit events to client

const {WS, WSController} = require('@tanhuy998/context-js');

@WS.context()
class Controller extends WScontroller{

    constructor() {

        super();
    }

    @WS.channel('message')
    async message() {

        await this.emit('confirm-message', {status: 'ok'});
    }
}

WS channeling events

When an event occurs, it's channeled to direct registered handlers. To channel events, use colon ":" to seperate subchannels.

const {WS, WSController} = require('@tanhuy998/context-js');

@WS.context()
class CartController extends WSController {

    constructor() {

      super();
    }

    @WS.channel('cart:addProduct')
    addProduct() {

        
      
    }

    @WS.channel('cart:checkout')
    checkout() {


    }
}

WS Prefix channels

Apply @WS.channel to the controller for declaring a prefix for its sub channels inside

const {WS, WSController} = require('@tanhuy998/context-js');

@WS.channel('cart')
@WS.context()
class Controller extends WSController {

    constructor() {

      super();
    }

    @WS.channel('addProduct')
    addProduct() {

        /**
         *  client.emit('cart:addProduct')
         */
      
    }

    @WS.channel('checkout')
    checkout() {

        /**
         *  client.emit('cart:checkout')
         */
    }
}

WS filter

Filters are middlewares that intercept the flow of handling an event. When a websocket connection established, filters will be invoked for each emitted event from the client to check for prerequisites.

Similar to standard js Array.filler method. Each filter function need to return a value of boolean (or values related to true/false) to tell the router that a particular event meets specific requirements for furhter handling operations.

Defining a filter.

function filter(_event) {

  return true // if pass, false otherwise
}

To add filter to Controller, use @WS.filter()

const {WS, WSController} = require('@tanhuy998/context-js');

function authorize(role = 'user') {

    return function (event) {

        const token = event.sender.handshake.auth:

        const secret = proccess.env.secret;

        try {

            const payload = jwt.verify(token, secret);

            if (payload.role === role) {

                return true;
            }

            return false;
        }
        catch(e) {

            return false;
        }
    }
}

@WS.filter(authorize('admin'))
@WS.context('/admin')
class Controller extends WSController {

    /*
     *  this controller will take effect on '/admin' namespace
     */

    constructor() {

      super();
    }

    @WS.channel('chat:message')
    handleMessage() {

        return {status: 'ok'};
    }
}

WS namespaces

The Socket.io module defines a concept called 'namespace' which is to isolate hanlding context betwwen clients.

Context-js also cover this concept for flexibly isolating activities acrosss controllers.

const {WS, WSController} = require('@tanhuy998/context-js');

/*
 *  By default, @WS.context() works on the default namespace called '/',
 *  to assign a namspace to the controller to take effect on.
 *  just pass the namespace(s) as argument(s) to @WS.context
 */

@WS.context('/admin')
class Controller extends WSController {

    /*
     *  this controller will take effect on '/admin' namespace
     */

    constructor() {

        super();
    }


    @WS.channel('chat:message')
    handleMessage() {

        return {status: 'ok'};
    }
}

another way of declaring namespaces is using @WS.namespace

const {WS, WSCont roller} = require('@tanhuy998/context-js');

/*
 *  By default, @WS.context() works on the default namespace called '/',
 *  to assign a namspace to the controller to take effect on.
 *  just pass the namespace(s) as argument(s) to @WS.context
 */

@WS.context()
@WS.namespace('/admin')
class Controller extends WSController {

    /*
     *  this controller not only takes effect on '/admin' namespace
     *  but also effects on the default namespace '/' because default 
     *  argument of @WS.context is '/'
     */

    constructor() {

      super();
    }

    @WS.channel('chat:message')
    handleMessage() {

        return {status: 'ok'};
    }
}

Error handling with WSControllers

Context-js provides an error handling mechanism that helps users handle errors as simple as possible.

By default, Context js will check for the existence of onError method of each controller as a default error handler that is thrown inside the controller.

Errors would be thrown back to the the higher order error handler if the controller didn't defining any error handler.

const {WS, WSController, args, WSEvent} = require('@tanhuy998/context-js');


@WS.context()
@WS.namespace('/admin')
class Controller extends WSController {

    constructor() {

      super();
    }

    @WS.channel('chat:message')
    @args(WSEvent)
    async handleMessage(_event) {

        await Model.Message.save(_event.eventArguments);

        return {status: 'ok'};
    }
    
    @args(Error) // inject the thrown error to the method
    onError(_error) {

      /*
       *  if Model.Message.save(_event.eventArguments) throw an error
       *  the Error will automatically been caught and dispatched to handler
       */

      console.log(_error.message);
    }
}

More Error Handlers

use @WS.handleError on the method you want it to be an error handler

const {WS, WSController, args, WSEvent} = require('@tanhuy998/context-js');

class CustomError extends Error {


}


@WS.context()
@WS.namespace('/admin')
class Controller extends WSController {

    constructor() {

      super();
    }

    @WS.channel('chat:message')
    @args(WSEvent)
    async handleMessage(_event) {

        await Model.Message.save(_event.eventArguments);

        return {status: 'ok'};
    }
    
    @args(Error)
    onError(_error) {

      /*
       *  if Model.Message.save(_event.eventArguments) throw an error
       *  the Error will automatically been caugtch
       */

      if (_error.code === 'ERR_BUFFER_OUT_OF_BOUNDS') {

        throw new CustomError();
      }

      throw _error;
    }

    @WS.handleError
    @args(Error)
    handleCustomError(_error) {

      if (_error instanceof CustomError) {

        console.log('error handling chain ends here');
      }
      else {

        return _error;
      }
    }

    @WS.handleError
    lastHandler() {

      const error = this.error;

      const next = this.context.next;

      const event = this.context.state;

      event.response({
        status: 'error'
      }); // response back to the client if the clients declared an acknowledgment

      // dispatch the error for the higher order error handler unit
      next(error);
    }
}

dispatching error in handlerchain

There are three ways to dispatch the error to next handler.

const {WS, WSController, args, WSEvent} = require('@tanhuy998/context-js');


@WS.context()
@WS.namespace('/admin')
class Controller extends WSController {

    constructor() {

      super();
    }
    
    @WS.handleError
    @args(Error, WSEvent)
    error(_error, _event) {


      this.context.next(_error);

      throw _error;

      return _error;
    }
}

*Note: injecting Error abstract on a method that is not an error handler would cause unexpecting result. In most cases the IocContainer would inject undefined.

Utility Decorators

Unstable @requestParam(...paramName : string) (works on both method and property)

Assign specific request.param to a specific instance (method and property).

Example

Apply to property

class Controller extends BaseControlle {

  @requestParam('userId')
  id;


  // in this case the "info" property
  // will be assigned to 
  // {
  //     userId: this.httpContext.userId,
  //     userName: this.httpContext.userName
  // }
  @requestParam('userId', 'userName')
  info;
}

Apply to method (deprecated)

class Controller extends BaseControlle {

    @requestParam('userId', 'userName')
    getUserInfo(id, name) {

      // the order of the params is stricted

      console.log(id, name);
    }
}

@consumes(field, value)

Register a middleware before an endpoint to check for request headers. Will response status 400 If the request headers do not match.

Example

@comsumes({
    'User-agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59'
})
someMethod() {
    
    // just handle request from Edge browser
}

@contentType(value : string)

Pre-set the response 'Content-Type' header, can be overwritten in controller action.

@header(name : string, value : string || array)

Pre-set the response header (using Express's res.setHeader method), headers can be overwritten in controller action

@Endpoint decorator

The @Endpoint is a "http method" friendly to mapping route. @Endpoint just focuses following methods:

@Endpoint.GET(path:string)
@Endpoint.HEAD(path:string)
@Endpoint.POST(path:string)
@Endpoint.PUT(path:string)
@Endpoint.DELETE(path:string)
@Endpoint.CONNECT(path:string)
@Endpoint.OPTIONS(path:string)
@Endpoint.TRACE(path:string)
@Endpoint.PATCH(path:string)

Author

License

MIT