Skip to content

Latest commit

 

History

History
190 lines (144 loc) · 5.7 KB

README.md

File metadata and controls

190 lines (144 loc) · 5.7 KB

Rules

Validate and Authorize

Rule and RuleHandler

Combining Rules

Validate and Authorize

Rules can be used to validate and authorize requests. Most requests require some checks to make sure the operation is safe and can be performed by the requester. For example, you wouldn't want a user updating another user's information, and you wouldn't want a request attempting to update a resource that doesn't exist, hit the database. So rules can be used as a gatekeeper for requests. They should not be used to manipulate the request (see Middleware) and do not resolve any values themselves.

A Rule must return a promise. If the promise is resolved with anything other than an error Restkit Response then the rule has been satisfied. If the promise is rejected or an Restkit Response with an error status code is resolve, the rule has failed.

Rule and RuleHandler

To create a Rule we use the RuleHandler decorator to define our handler method. The RuleHandler method is an injectable function, so we can inject properties from the route context. A RuleHandler must be given a name, this name should describe the rule being performed.

import {Auth, Body, RuleHandler, Response} from 'expresskit';

export class UserService {
  @RuleHandler('IsUserOwner')
  public static isUserOwner(@Auth('User') auth: any, @Body() user: any): Promise<void> {
    return new Promise((resolve, reject) => {
      if(auth.userId === user.id) {
        resolve();
      } else {
        reject();

        // OR

        resolve(new Response(403, `This isn't you.`));

        // OR

        reject(`This isn't you.`);
      }
    });
  }
}

As mentioned before, any resolved promise is a PASS and any rejected promise is a FAIL. The exception is if an Restkit Response is resolved with an error status code. By default, all failed rules response with a 500 error. If an Restkit Response is used, the status code given will be used.

With the IsUserOwner rule defined we can now apply it to any route method.

// Make sure the compiler knows to include this at some point. If the UserService
// is never used directly, we will need to import the file like shown-
import './user.service';

export class UserRouter {
  @Route('PUT', '/user')
  @Rule('IsUserOwner')
  public static updateUser(@Body() user: any) {
      // update user
  }
}

With Rules we can cut down on the code needed to validate requests, and focus on the logic of fulfilling the requet.

Combining Rules

A route may have multiple rules. Instead of creating a Rule for each route, you can create specialized rules that can be mixed and matched with others. Lets say we are selling Widgets and on a purchase of a Widget we need to make sure that Widget is in stock and the User has enough money in their account. We can create a CanPurchaseWidget Rule, or we can create two rules- IsWidgetStocked and UserHasFunds. To illustrate this, we will have two methods, purchaseWidget and addWidgetToCart.

export class WidgetService {
  @RuleHandler('IsWidgetStocked')
  public static isWidgetStocked(@Param('widgetId') widgetId: number): Promise<void> {
    let widget = (lookup widget from database);
    if(widget.stock > 0) {
      resolve();
    } else {
      reject('Widget is out of stock.');
    }
  }

  @RuleHandler('UserHasFunds')
  public static userHasFunds(@Param('widgetId') widgetId: number, @Auth('User') auth: any): Promise<void> {
    let widget = (lookup widget from database);
    let user = (lookup user from database);

    if(user.funds >= widget.price) {
      resolve();
    } else {
      reject('There is not enough money in your account to purchase this widget.');
    }
  }
}

export class WidgetRouter {
  @Route('POST', '/widget/:widgetId/purchase')
  @Rule('IsWidgetStocked')
  @Rule('UserHasFunds')
  public static purchaseWidget(@Param('widgetId') widgetId: number, @Auth('User') auth: any): Promise<void> {
    // all good- purchase widget
  }

  @Route('POST', '/user/cart/:widgetId')
  @Rule('IsWidgetStocked')
  public static addWidgetToCart(@Param('widgetId') widgetId: number, @Auth('User') auth: any) {
    // all good- add to cart
  }
}

Multiple rules behave like an AND operation. All rules must pass to continue the route. If you need an OR operation you can add multiple rules to the same Rule decorator.

  // Only one rule needed to pass to continue the route 
  @Route('GET', '/foo')
  @Rule('RuleOne', 'RuleTwo', 'RuleThree')
  public static fooRoute() {}

  // All rules must pass to continue the route
  @Route('GET', '/foo2')
  @Rule('RuleOne')
  @Rule('RuleTwo')
  @Rule('RuleThree')
  public static fooRoute2() {}

  // RuleOne OR RuleTwo must pass in addition to RuleThree 
  @Route('GET', '/foo2')
  @Rule('RuleOne', 'RuleTwo')
  @Rule('RuleThree')
  public static fooRoute2() {}

It is recommended that rules are focused on single conditions when possible to allow better flexibility, readbility, and testibility.

NOTE: If performance is a concern with the redundant resolutions and database calls, this will be fixed with Resolutions in a future update.

Keep Reading

Routing

Response

Middleware

Resource Resolutions

Rules

DTOs

More Links

Expresskit Seed Project)

Github

Issues

NPM

Twitter