1.4.2 • Published 3 years ago

telar-mvc v1.4.2

Weekly downloads
10
License
MIT
Repository
github
Last release
3 years ago

Telar MVC

npm npm npm

Lightweight powerful implementation of MVC(Model-View-Controller) for Node servers. Inspired and a fork from inversify-controller.

Requirements

  • node >= 7.10
  • typescript >= 2.4

Installation

  1. Install prerequire packages
npm i koa @koa/router ajv reflect-metadata telar-mvc
  1. Install IoC container

    • e.g. Inversify Conainter

      npm i inversify
  2. Make sure to import reflect-metadata before using telar-mvc:

import "reflect-metadata";
  1. Must add below options in your tsconfig.json:
{
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
}

New features

Usage

Creating a controller

import { Controller, path } from 'telar-mvc';

@Path('/')
class HomeController extends Controller {
  
}

Binding your controller to your application using Inversify container

The bind() helper correlates your app, your container and your controllers.

import { bind, IController, Controller } from 'telar-mvc';
import * as Koa from 'koa';
import { container } from './inversify.config';
import { HomeController, UserController, ProductController  } from './controllers';

// NOTE: Make sure to decorate the `Controller` class from `tela-mvc` if you are using inversify. 
// This line should run right before binding and only should run ONE time!!!
decorate(injectable(), Controller);

const identifiers = {
    HomeController: Symbol('HomeController'),
    UserController: Symbol('UserController'),
    ProductController: Symbol('ProductController'),
};

this.container.bind<IController>(identifiers.HomeController).to(HomeController);
this.container.bind<IController>(identifiers.UserController).to(UserController);
this.container.bind<IController>(identifiers.ProductController).to(ProductController);

const app = new Koa();

/**
 * app: Koa app
 * container: Implementation of IContainer or any IoC Container implemented `get<T>(TYPE)` method like Inversify or TypeDI
 * controller identifiers: A list of controller identifiers 
 * NOTE: This is required and must happen early in your application, ideally right after your create your app
 */
bind(app, container, [identifiers.HomeController, identifiers.UserController, identifiers.ProductController]);

Middlewares

This module encourages you to declare middlewares at the controller level (vs. at the app level). This gives you the same result as if you were using app.use(), but keeps everything in the same place.

import { before, after } from 'telar-mvc';
import { bodyParser } from 'koa-bodyparser';
import { errorHandler } from '../error-handler';

@Before(bodyParser())
@After(errorHandler())
@Path('/')
class HomeController extends Controller {
  
}

Below we register a middleware that's specific to the UsersController.

@Path('/users')
@Before(logMiddleware) // Only /users routes (and descendants) will be affected
class UsersController extends Controller {
  
}

Passing arguments to middlewares

There are times where you need to inject some properties into a middleware, which properties are accessible in the controller itself. @BeforeFactory and @AfterFactory allow you to differ a middleware's creation.

@BeforeFactory(function(this: HomeController) { // Notice the usage of a regular function
  return logMiddlewareFactory(this.logger);
})
class HomeController extends Controller {
  @inject(Identifiers.LoggerService) public logger: ILoggerService;  
}

Route level middlewares

Route level middlewares are declared the exact same way as controller middlewares, using @Before, @After and @BeforeFactory. @AfterFactory is currently not supported.

class UsersController extends Controller {
  private foo: string = 'bar';
  
  @Get('/')
  @Before(queryParser())
  @Before(async function(this: UsersController, ctx: RouterContext, next: Next) {
    console.log(this.foo); // bar
    await next();
  })
  public async list(ctx: RouterContext) {
    
  }
}

Declaring routes

This module offers http decorators for all HTTP verbs. Check each decorator's documentation for specific options.

class UsersController extends Controller {
  
  @Get('/')
  public async list(ctx: RouterContext) {
    
  }
  
  @Get('/:id')
  public async getById(ctx: RouterContext) {
      
  }
  
  @Post('/')
  public async add(ctx: RouterContext) {
      
  }
  
  @Del('/:id')
  public async removeById(ctx: RouterContext) {
    
  }
  
  // ...
}

Action results

To make the code clean for proccessing http response, we provided some functions like jsonResult return json body, contentResult return string body, redirectResult redirect response. You also can return string/json type in the action function following example below.

import { jsonResult, contentResult, redirectResult } 'telar-mvc';

class UsersController extends Contoller {
  @Post('/:id')
  public async action1(ctx: RouterContext) {
    try {
      return jsonResult({success: true})
    } cache(error) {
      return jsonResult(error, {status: 400})
    }
  }

  @Post('/:id')
  public async action1(ctx: RouterContext) {
    try {
      return contentResult('it was successful')
    } cache(error) {
      return contentResult('it was not successful', {status: 400})
    }
  }

  @Post('/:id')
  public async action1(ctx: RouterContext) {
    try {
      return redirectResult('/page/success')
    } cache(error) {
      return redirectResult('/page/not-success')
    }
  }

  @Post('/:id')
  public async action1(ctx: RouterContext) {
    try {
      return 'it was successful' // convert to content result
    } cache(error) {
      return {
        body: 'it was not successful'
        status: 400,
        headers: []
      } // response with in plain json is valid
    }
  }
}

Class model validation

You can use decorators for your class model(from MVC) to validate your request body. We use ajv-class-validator to change conver json to object and validate. The model object is injected in the context.

Note: to use class model validation you need to add body parser middleware, if you are using for Koa you can install koa-bodyparser

import { ActionModel, jsonResult } 'telar-mvc';
import { MaxLength, Required } from 'ajv-class-validator';

export class User {
   
  @MaxLength(15)
  public name: string 

  constructor(
      @Required()
      public id: string,
  ) {
    this.id = id
  }
}

class UsersController extends Contoller {
  @Get('/:id')
  @ActionModel(User) // <---- Should define to access model in `ctx: RouterContext`
  public async save({ model }: Context<User>) {
    if (model.validate()) {
      db.save(model);
      return jsonResult({success: true})
    } else {
      console.log(model.errors()); // output errors - if options can passed to AJV `{allErrors: true}` you will have the list of errors
      return jsonResult(error, {status: 400})
    }
  }
}

Path parameters validation

Path parameters have to be declared in your route's path. Additionally, this module offers validation and type coercion through JSON schemas and the @Params() decorator.

import { object, integer, requireProperties } from '@bluejay/schema'; 

class UsersController extends Contoller {
  @Get('/:id')
  @Params(requireProperties(object({ id: integer() }), ['id']))
  public async getById(ctx: RouterContext) {
    const { id } = req.params;
    console.log(typeof id); // number, thanks to Ajv's "coerceTypes" option
  }
}

An instance of AJV is created by default, if you want to pass your own, provide a ajvFactory to the options.

import { object, integer, requireProperties } from '@bluejay/schema';
import * as Ajv from 'ajv';

const idParamSchema = object({ id: integer() });

class UsersController extends Contoller {
  @Get('/:id')
  @Params({
    jsonSchema: requireProperties(idParamSchema, ['id']),
    ajvFactory: () => new Ajv({ coerceTypes: true })
  })
  public async getById(ctx: RouterContext) {
    const { id } = req.params;
    console.log(typeof id); // number
  }
}

If the params don't match jsonSchema, a BadRequest error will be thrown and be handled by your error middleware, meaning that your handler will never be called.

Query parameters validation

Query parameters are validated through JSON schemas using the @Query() decorator.

All HTTP method decorators also accept an optional query option with the same signature as the decorator.

import { object, boolean } from '@bluejay/schema';

class UsersController extends Controller {
  @Get('/')
  @Query(object({ active: boolean() }))
  public async list(ctx: RouterContext) {
    const { active } = req.query;
    console.log(typeof active); // boolean | undefined (since not required)
  }
}

A BadRequest error will be thrown in case the query doesn't match the described schema, in which case your handler will never be called.

Grouping query parameters

Groups allow you to group properties from the query object and are managed by a groups hash of the form { [groupName]: groupProperties }.

Those come handful if your application exposes complex query parameters to the end user, and you need to pass different properties to different parts of your application.

class UsersController extends Controller {
  
  @Query({
    jsonSchema: object({ active: boolean(), token: string() }),
    groups: { filters: ['active'] }
  })
  @Get('/')
  public async list(ctx: RouterContext) {
    const { filters, token } = req.query;
    console.log(typeof token); // string
    console.log(typeof filters); // object
    console.log(typeof filters.active); // boolean | undefined (since not required)
    console.log(req.query.active); // undefined (grouped properties are removed from the root query)
  }
}
Transforming query parameters

transform allows you to process and modify the query string before the groups are formed. This is useful if, for example the interface your application offers to its consumers differs from the interface used within the application.

Note: The transform hook is called before parameters are grouped. Also note that you can use transform without groups.

const queryTransformer = (query: object) => {
  query.active = query.isActive;
  delete query.isActive; // Clean
  return query;
};

class UsersController extends Controller {
  @Query({
    jsonSchema: object({ isActive: boolean() }),
    transform: queryTransformer
  })
  @Get('/')
  public async list(ctx: RouterContext) {
    const { active } = req.query;
    console.log(typeof active); // boolean
    console.log(typeof req.query.isActive); // undefined
  }
}

Body validation

We currently only offer validation for JSON body. You can declare bodies of another type, but you will need to handle the validation by yourself. Bodies are managed through the @Body() decorator.

JSON body

const userSchema = object({
  email: email(),
  password: string(),
  first_name: string({ nullable: true }),
  last_name: string({ nullable: true })
});

class UsersController extends Controller {
  @Post('/')
  @Body(requireProperties(userSchema, ['email', 'password']))
  public async add(ctx: RouterContext) {
    // req.body is guaranteed to match the described schema
  }
}

A BadRequest error will be thrown in case the body doesn't match the described schema, in which case your handler will never be called.

Other types

The only validation possible for now is the content type, and this is done via the @Is decorator, which validates the content type of the body.

class UsersController extends Controller {
  @Put('/:id/picture')
  @Is('image/jpg')
  @Before(multer.single('file')) // Just an example, see https://www.npmjs.com/package/@koa/multer
  public async changePicture(ctx: RouterContext) {
    
  }
}

API Documentation

TODO