advanced-controllers v2.5.0
Advanced Controllers
Features:
- MVC-like controllers for Express
- Easy configuration through
@Decorators - Data binding for request data (query, body, params)
- Can handle
asyncfunctions and Promises - Return value handling (data, exception, Promise)
- Authorization support (custom or roles and permissions)
- Written in TypeScript, compiled to ES5
See the tests for examples. See the Change log for breaking changes.
Not documented yet
- Adding custom validators
MVC Features
Inspired on ASP.NET MVC it is possible to create Express based controllers and actions.
import * as web from 'advanced-controllers';
@web.Controller('/kitten')
class KittenController extends AdvancedController {
// GET /kitten/all
@web.Get('/all')
getAll() { }
// GET /kitten/details
@web.Get()
details() {}
// POST /kitten/create
@web.Post()
create() {}
// DELETE /kitten/delete
@web.Del('delete')
deleteKitten() {}
}
let kittenCtrl = new KittenController();
kittenCtrl.register(expressApp);Features:
- Use the
@Controller('name')decorator on the controller class - Use the following functions:
Get,Post,Put,Head,Options,Del - The beginning
/character in the names is optional - The method names can be omitted. In this case the function name is used
Caveats:
- You have to actually call these functions. Good:
@Get(), bad:@Get - Use
@Delinstead of@Delete - Don't forget to inherit from
AdvancedControllerand call theregister()function - You forget to initialize
body-parserfor your Express app (so body won't be parsed)
Registration details
You can register a single controller or all AdvancedController instances created so far.
kittenCtrl.register(
expressApp, // Express app
{}, // Optional settings
);
// OR
AdvancedController.registerAll(expressApp, {});Optional settings can have these props:
namespace: Prefix for the routes (E.g.namespace1-->/namespace1/ctrl/action)debugLogger: Debug logger for registrationerrorLogger: Any runtime errors (E.g. Errors / promise rejects in actions -- exceptWebErrorinstances, see below at theWebErrorsection)implicitPublic: See below at Permissions
Caveats:
- If you use
AdvancedController.register, use it once and do not use the instances'registermethods. AdvancedController.registerworks on instantiated controllers.
Data Binding
Tired of calling and validating let myStuff = req.body.someVariable in every function? Well, we try to make it a bit more comfortable. We have some limitations though but here's what we've got:
import * as web from 'advanced-controllers';
@web.Controller('/kitten')
class KittenController extends web.AdvancedController {
// GET /kittens/all?from=0[&cnt=25]
@web.Get('/all')
getKittens(
@web.Query('from', Number) from: number,
@web.Query('cnt', Number, true) cnt: number
) { }
// POST /kittens/create, body: { kitten: {} }
@web.Post()
create(
@web.Body('kitten', Object) kitten: Kitten
) {}
// POST /kittens/create2, body: {}
@web.Post()
create2(@web.Body() kitten: Kitten)
// DELETE /kittens/delete/<id>
@web.Del('delete/:id')
deleteKitten(@web.Param('id') id: string) {}
}Interface
// Whole body
export function Body(type?: any): ActionDecorator;
// Member of body object
export function Body(name: string, type?: any, optional?: boolean): ActionDecorator;
// Query winthout type
export function Query(name: string, optional?: boolean): ActionDecorator;
// Query with type
export function Query(name: string, type: any, optional?: boolean): ActionDecorator;
// Param with or without type
export function Param(name: string, type?: any): ActionDecorator;Features:
- Type parameter is optional
- In
Body: only validation - In
QueryandParam: parse + validation - Supported types by default:
String,Number,Object,Array
- In
- If there is no type for
QueryandParamthen the parameter value will be astring - In
QueryandParam: Objects and arrays in query MUST be JSON-serialized. But seriously... arrays and objects in query? - The bound value must be present unless you set the
optionalparameter totrue
Caveats:
- Don't forget about the parentheses... Good:
@Body(), bad:@body - You MUST add the variable name when parsing body or query parameter. We cannot parse it for you
- Well, ehm... actually we could (like the Angular team did) but currently we don't want to. It's kinda ugly. Maybe later
Original Request and Response
You can access the original req or res objects with similar syntax. Beware: if you ask the res object then we won't handle the return values for you. (Return values are discussed soon.)
@web.Controller('casual')
class CasualController extends web.AdvancedController {
@web.Get('fancy-function')
fancyFunction(
@web.Req() req: web.Request,
@web.Res() res: web.Response
) {}
}Authenticated Users
There's a @Auth shorthand which returns the request.auth object (or undefined). The value of the user is usually set in an express middleware such as express-jwt.
Note: express-jwt versions below 7 used request.user, but from 7+ they use request.auth. The @User shorthand (deprecated) returns whichever it finds, the @Auth works with versions 7+.
@web.Controller('casual2')
class CasualController extends web.AdvancedController {
@web.Get('latest-fancy-function')
latestFancyFunction(@web.Auth() auth?: { id: string }) {
console.log(`Gotcha: ${user ? user.id : 'nevermind'}`);
}
// Deprecated
@web.Get('another-fancy-function')
anotherFancyFunction(@web.User() user?: { id: string }) {
console.log(`Gotcha: ${user ? user.id : 'nevermind'}`);
}
}Caveats:
- If you use
resthen you have to manually end the request, e.g.res.send('')(see next section) - Parentheses... Good:
@Req(), bad:@Req - The
@Authand the@User()decorators returnundefinedby default. You'll need anexpress-jwtto have anything there.
Return values
By default the response is closed automatically with a status code and sometimes with data
- Async actions / Promise results are awaited first
- When the action executes correctly, depending on the return value
- On
undefined: 200 - On
stringtypes: 200 + result as body (raw string) - Otherwise: 200 + value
JSON.strigify-ed
- On
- Missing bound parameter: 400
- When the action throws an error:
- If the error has a
statusCodeproperty ending withstatusCode* You should use theWebErrorclass for this - Otherwise 500
- If the error has a
jsonfield then it will be sent as JSON - If the error has a
textfield then it will be sent as plain text - Default error parsing error sends back
{ "errors": [ { "message": "some-stuff" }]}
- If the error has a
Caveats:
- If the action asked for
resthen there is no auto-close. In this case we don't know whether the response is closed -- or will be closed -- in the action. The only exception is if the action throws an error: in that case we apply the regular error handler logic.
WebError object
This object extends Error and can be used to conveniently send back custom HTTP codes, error messages and codes. Feel free to throw it in actions, the framework will handle it.
new WebError(message: string), sending HTTP status code 500 by defaultnew WebError(message: string, statusCode: number)new WebError(message: string, settings: { statusCode?: number, errorCode?: number | string}), theerrorCodewill be in the result JSON aserrors[0].code
The result will be something like this: { "errors": [ { "message": "some-stuff", "code": 1 }]} + HTTP 400 header
Customization by overwriting WebError.requestErrorTransformer.
Middlewares
One extra functionality is the utilization of express.use(). You can specify middleware calls over the actions.
@web.Controller('middleware')
class MiddlewareTestController extends web.AdvancedController {
middleware(req: web.Request, res: web.Response, next: Function) {
console.log('Middleware called');
next();
}
@web.Get('do-stuff')
@web.Middleware('middleware')
doStuff() {}
@web.Get('do-more-stuff')
@web.Middleware(otherMiddlewareFunction)
doMoreStuff() {}
}Functions:
- Call a controller function -- function name (as string) in parameter
- Call directly a function -- function reference in parameter
- In both cases on middleware call the
thisreference will be the controller instance
Limitations:
- There's no way to add global middleware, won't be, do it manually
- There's no way to add class-specific middleware (this might change)
- There's no way to add independent middlewares with custom / RegEx routes, do it manually
Caveats:
- A tricky one: if you specify the middleware function with Arrow Syntax (
() => {}) then thethisreference won't refer to the controller instance when the middleware is called. The reason is TS/ES6 to ES5 transpilation: thethisreference changes in the process and I could not bind it.
Permissions
Action authorization can be controlled by the following decorators. Controller classes and action functions can be decorated (the latter is stronger). An action will have a single permission. (You shouldn't use multiple decorators on the same class or action function.)
@Permission(name?: string): The action requires thenamepermission (default value:ctrl.action)@Authorize(): The action requires an authenticated user, i.e.req.userobject must not beundefined@Public(): The action does not require
The permission check can be managed 2 ways.
- Default: custom permission check. Use a custom middleware before registering the controllers creating the following function on the request object:
req.user.hasPermission(permission: string): boolean | Promise<booleam>. You can implement it however you'd like to - Role based: Call the
AdvancedController.setRoles(roles: { name: string, permissions: string[] }[])function to set the roles and their permissions. The following array should exists:req.user.roles: string[]
export interface RequestWithUser extends Request {
user: {
// Default: custom authorization
hasPermission?(permission: string): boolean | Promise<boolean>;
// Role-based authorization
roles?: string[]
};
}You should create the req.user.hasPermission function OR the req.user.roles array in your custom authorization middleware.
A security enforcement
If you don't use permissions (@Permission or @Authorize or @Public) then you can ignore this subsection.
If there are permission-related decorators in your app then you shall do at least one of the following to avoid auto-authentication:
- Decorate public functions (or controllers) with the
@Publicdecorator - Register the controllers with
implicitPublic, e.g.:AdvancedController.regiseterAll(app, { implicitPublic: true })
This is to prevent unintentional publication of some of your actions by forgetting the @Permission decorator.
Example
You can use permissions like this:
// You can annotate this
@web.Controller('perm')
class PermissionController extends web.AdvancedController {
// GET /perm
// Needs permission: 'perm.testEmpty'
@web.Permission()
@web.Get('')
testEmpty { return { done: true }; }
// GET /perm/test1-a
// Needs permission: 'perm.test1-a'
@web.Permission()
@web.Get('test1-a')
testOneA() { return { done: true }; }
// GET /perm/testOneB
// Needs permission: 'perm.TestOneB'
@web.Permission()
@web.Get()
testOneB() { return { done: true }; }
// POST /perm/test2
// Needs permission: 'perm.test-two'
@web.Permission('perm.test-two')
@web.Post('test2')
testTwo() { return { done: true }; }
// GET /perm/noPerm
// NO permission required
@web.Get('noperm')
@web.Public()
noPerm() { return { done: true }; }
// GET /perm/authorized
// Must be authenticated, but no explicit permission required
@web.Get('authorized')
@web.Authorize()
authorized() { return { done: true}; }
}Static functions
The following static functions help the management of authorization:
AdvancedController.getAllPermissions(): string[]: Aggregates thegetPermission()results for allAdvancedControllerinstances.AdvancedController.getAllPublicRoutes(): string[]: Aggregates thegetPublicRoutes()results for allAdvancedControllerinstances. This can be used to create a whitelist in he authentication middleware (e.g. skip JWT checks on these URLs). Note that the results contain/ctrl/actionstyle URLs but they do NOT contain thenamespaceif there is any (i.e. NOT/namespace/ctrl/action).
Note that the static functions work on instantiated controllers only.
Notes and Caveats
Notes:
- Note that this package authorizes but NOT authenticates.
- Note that only one permission check method works at a time. By default:
hasPermission. AfterAdvancedController.setRolesis called: role-based. - The permission-related decorator can be used on classes as well. It won't override the action-level permissions though.
- When the user is not authenticated (most commonly:
req.userdoes not exists) the response is: 401{ errors: [{ message: "Unauthenticated" }]}. - When the user is authenticated but does not have the required permissions the response is: 403
{ errors: [{ message: "Unauthorized" }]}.
Caveats:
- Register the middleware providing
req.user.hasPermissionorreq.user.rolesbefore registering the controller. - On empty action name: the permission will have the name of the function instead of the name of the action (which is
'')
3 years ago
3 years ago
5 years ago
7 years ago
7 years ago
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago