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.
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.
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.