Package Exports
- event_request/server/components/caching/data_server
- event_request/server/components/error/error_handler
- event_request/server/components/session/session
- event_request/server/components/validation/validation_result
- event_request/server/plugins/available_plugins/data_server_plugin
- event_request/server/plugins/available_plugins/rate_limits_plugin
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (event_request) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
EventRequest
A highly customizable backend server in NodeJs
Setup
// Framework Singleton instance
const app = require( 'event_request' )();
const { Loggur } = require( 'event_request' );
// Add a new Route
app.get( '/', ( event ) => {
event.send( '<h1>Hello World!</h1>' );
});
// Start Listening
app.listen( 80, ()=>{
Loggur.log( 'Server started' );
});The framework has a web server Singleton instance that you can fetch from anywhere using:
// First way
const app = require( 'event_request' )();
// Second way ( you can use this when you want to fetch multiple things from the framework )
const { App } = require( 'event_request' );
const app = App();Multiple Servers Setup:
const { Server, Loggur } = require( 'event_request' );
/**
* @brief Instantiate the server
*/
// With this setup you'll have to work only with the variables appOne and appTwo. You cannot call Server() to get any of them in different parts of the project
// This can be remedied a bit by creating routers in different controllers and then exporting them to be later on added
const appOne = new Server();
const appTwo = new Server();
// Add a new Route
appOne.get( '/', ( event ) => {
event.send( '<h1>Hello World!</h1>' );
});
// Add a new Route
appTwo.get( '/', ( event ) => {
event.send( '<h1>Hello World x2!</h1>' );
});
appOne.listen( 3334, ()=>{
Loggur.log( 'Server one started at port: 3334' );
} );
appTwo.listen( 3335, ()=>{
Loggur.log( 'Server two started at port: 3335' );
} );Custom http Server setup:
const http = require( 'http' );
const { App, Loggur } = require( 'event_request' );
const app = http.createServer( App().attach() );
// No problem adding routes like this
Server().get( '/',( event )=>{
event.send( 'ok' );
} );
// OR like this
app.get( '/test',( event )=>{
event.send( 'ok' );
} );
app.listen( '80',()=>{
Loggur.log( 'Server is up and running on port 80' )
});Plugins:
- https://www.npmjs.com/package/er_memcached_data_server - memcached data server
Example Projects:
- https://github.com/Michaelpalacce/Server - A Web App that emulates a File System on your browser and can be used to upload/download/delete files, images, audio and etc as well as stream videos directly from your browser
- https://github.com/Michaelpalacce/ChatApp - An unfinished chat app using a combination of EventRequest and Socket.IO
#Properties exported by the Module: App, // The Singleton Instance of the Framework that can be used to retrieve the Web Server at any point
Server, // The Server of the framework. This is just the class and not the actual isntance. Use this only if you want to create multiple servers
Testing, // Testing tools ( Mock, Tester( constructor ), logger( logger used by the testing suite ), test( function to use to add tests ), runAllTests( way to run all tests added by test )
Logging, // Contains helpful logging functions
Loggur, // Easier access to the Logging.Loggur instance#Getting started:
Setup
The Framework uses the "Singleton" design pattern to create a web server that can be retrieved from anywhere. When requiring the module a callback is returned that if called will return the instance:
const app = require( 'event_request' )();Parts of the framework:
Middlewares are callbacks attached to specific routes. They are called in order of addition. Global middlewares are the same as normal middlewares but they are defined differently than the normal middlewares and are attached to them. They are called before the middleware they are attached to.
Dynamic Middlewares are added together with the Global Middlewares but they are functions ( or an array of functions ). They can be used to attach dynamic functionality to routes
EventRequest is an object passed to each middleware. It holds all data about the request and has a lot of helpful functionality used for sending responses.
The framework has a set of components. These components are individual and can be used outside of the framework.
The framework also has a set of plugins that are pre included. Each Plugin modifies the Server / Event Request a bit and "plugs in" new functionality.
#Components:
- Components are parts of the server that can be used standalone or extended and replaced ( mostly )
- Any component can be retrieved from : event_request/server/components
- Extendable components are :
- body_parsers => require( 'event_request/server/components/body_parsers/body_parser' )
- caching => require( 'event_request/server/components/caching/data_server' )
- error => require( 'event_request/server/components/error/error_handler' )
- file_streams => require( 'event_request/server/components/file_streams/file_stream' )
- rate_limiter => require( 'event_request/server/components/rate_limiter/bucket' )
#Plugins:
- Plugins are parts of the server that attach functionality to the EventRequest
- They can be retrieved from event_request/server/plugins
- They can also be retrieved from Server() ( look at the plugins section for more info )
#Router and Routing
- The router is used to route request to the appropriate middleware
Router Caching:
- The router has an internal middleware chain cache ( in case of repeating requests the middlewares that have to be executed will be cached. This is done to speed up the matching ).
- The cache also saves any params (router wildcards and RegExp matches ) and sets them back in the next request if the cache is hit.
- The keys to be saved can be configured
- The caching can be turned on or off
- The keys will be deleted if they have not been hit for more than one hour
- The router will attempt to clear the cache of stagnated entries on every request but it will get triggered at most every minute
####Functions exported by the Router:
static matchRoute( String requestedRoute, String|RegExp route, Object matchedParams ={} ): Boolean
- Match the given route and returns any route parameters passed in the matchedParams argument.
- Returns bool if there was a successful match
- The matched parameters will look like this: { value: 'key' }
- If the route is a RegExp then the matched parameters will contain the result of it and look like: { match: regExpResult }
matchRoute( String requestedRoute, String|RegExp route, Object matchedParams ={} ): Boolean
- Same as static
static matchMethod( String requestedMethod, String|Array|RegExp method )
- Matches the requested method with the ones set in the event and returns if there was a match or no.
matchMethod( String requestedMethod, String|Array|RegExp method )
- Same as static
define( String middlewareName, Function middleware ): Router
- Defines a global middleware
- Throws if a middleware with that name already exists
get/post/head/put/delete...( String route, Function middleware ): void
- Defines a get/post/head/put/delete middlewares for the given route
enableCaching( Boolean enable = true ): void
- Enables or disables the middleware block caching mechanism
setKeyLimit( Number keyLimit = 5000 ): void
- Sets the amount of keys the cache will have
- One key is a combination of the event.path and the event.method
####Adding routes
The server has 2 ways of adding routes/middleware
You can use .post, .put, .get, .delete, .head, .patch, .copy methods from the server that accept Required parameters: ( String|RegExp route, Function handler, Array||String middlewares = [] )
ALTERNATIVELY You can use those methods with the following commands: ( Function handler, Array||String middlewares = [] ) which will add a middleware to every route
const { App, Loggur } = require( 'event_request' );
const app = App();
app.define( 'default', ( event )=>{
Loggur.log( 'Hit the default global middleware' );
event.next();
});
app.define( 'defaultTwo', ( event )=>{
Loggur.log( 'Hit the second default global middleware' );
event.next();
});
app.get( ( event )=>{
Loggur.log( 'Always hits if method is get' );
event.next();
});
app.get( 'default', ( event )=>{
Loggur.log( 'After hitting the default global middleware' );
event.next();
} );
app.get( ['default', 'defaultTwo'], ( event )=>{
Loggur.log( 'After hitting the default global middleware with an ARRAY!' );
event.next();
} );
app.get( '/', ( event )=>{
event.send( '<h1>Hello World!</h1>');
});
app.post( '/', ( event )=>{
event.send( 'ok' );
});
app.delete( '/', ( event )=>{
event.send( 'ok' );
});
app.head( '/', ( event )=>{
event.send( 'ok' );
});
app.put( '/', ( event )=>{
event.send( 'ok' );
});
app.get( '/users/:user:', ( event )=>{
Loggur.log( event.params, null, true ); // Will print out whatever is passed in the url ( /users/John => 'John' )
event.send( event.params );
});
app.get( /\/pa/, ( event )=>{
Loggur.log( event.params, null, true );
// Will log { match: [ '/pa', index: 0, input: '/path', groups: undefined ] } if you hit the route /path
event.send( 'ok' );
});
app.listen( 80, ()=>{
Loggur.log( 'Server Started, try going to http://localhost and then to http://localhost/path. Don\'t forget to check the console logs ' );
});- When adding a Route the server.add( Object route ) or router.add( Object route ) can be used.
- The following parameters can be used when using .add():
####OBJECT CONTAINING:
handler: Function
- The callback function
- Required
route: String|RegExp
- The route to match
- Optional if omitted the handler will be called on every request
method: String|Array[String]
- The method(s) to be matched for the route
- Optional if omitted the handler will be called on every request as long as the route matches
middlewares: String|Function|Array[Function]|Array[String]
The global middlewares if any to be called before this middleware
Optional if omitted none will be called
server.add accepts a object that must contain handler but route, method and middlewares are optional.
( { method: '', route: '', handler:()=>{} } )
####Adding Routers:
- A router can be added by calling .add on another router: ( Router router )
- All the new router's routes will be added to the old one
- All the global middleware will be merged as well
const Server = require( 'event_request' );
Server().add( router );
// OR
const routerOne = Server().Router();
const routerTwo = Server().Router();
routerOne.add( routerTwo )####Adding Routers with path:
- A router can be added by calling .add on another router with a string route: ( String route, Router router )
- All the new router's routes will be pefixed with the given route
- All the global middleware will be merged as well
const Server = require( 'event_request' );
const routerOne = Server().Router();
const routerTwo = Server().Router();
routerOne.add( '/test', routerTwo )const Server = require( 'event_request' );
// You can also attach the router to a route
const userRouter = Server().Router();
userRouter.get( '/list', ( event )=>{
event.send( { userOne: {}, userTwo: {} } );
});
userRouter.post( '/add/:username:', ( event )=>{
// Add user to db or somewhere else
event.send( 'ok' );
});
Server().add( '/user', userRouter );####FUNCTION:
- server.add can also accept a function that will be transformed into a route without method or route ( Function route )
const Server = require( 'event_request' );
const routerOne = Server().Router();
routerOne.add( ( event )=>{
event.next()
});const Server = require( 'event_request' );
// Adding a route
Server().add({
route : '/',
method : 'GET',
handler : ( event ) => {
event.send( '<h1>Hello World!</h1>' )
}
});const Server = require( 'event_request' );
// You can create your own router
const router = Server().Router();
router.add({
route : '/',
method : 'GET',
handler : ( event)=>{
event.send( '<h1>Hello World</h1>' );
}
});const Server = require( 'event_request' );
// Adding a middleware without a method or route
Server().add( ( event )=>{
event.next();
});
Server().add( ( event )=>{
event.send( '<h1>Hello World x2!</h1>' );
});const Server = require( 'event_request' );
// To attach a router to the server simply call the add function of the server.
// Just like you would do to add a normal route.
Server().add( router );const Server = require( 'event_request' );
// You can also get the router attached to the Server and use that directly
const serverRouter = Server().router;
serverRouter.add(
//
);const App = require( 'event_request' );
// You can also attach the router to a route
const userRouter = App().Router();
userRouter.get( '/list', ( event )=>{
event.send( { userOne: {}, userTwo: {} } );
});
userRouter.post( '/add/:username:', ( event )=>{
// Add user to db or somewhere else
event.send( 'ok' );
});
App().add( '/user', userRouter );####Router Wildcards
- The route url can have a part separated by ":" on both sides that will be extracted and set to event.params
const { App, Loggur } = require( 'event_request' );
App().add({
route : '/todos/🆔',
method : 'GET',
handler: ( event)=>{
Loggur.log( event.params.id );
event.send( '<h1>Hello World</h1>' );
}
});
// Or
App().get( '/todos/🆔', ( event)=>{
Loggur.log( event.params.id );
event.send( '<h1>Hello World</h1>' );
});####Router Global and Dynamic middlewares:
You can
definemiddlewares in any router or the server. Middlewares will be merged if you add a router to another router.These global middlewares can be used to call a function before another step in the chain.You can add multiple middlewares per route.
Dynamic middlewares will accept the same parameters as Global Middlewares
When adding global middlewares to routes they can either be a single string or multiple strings in an array.
When adding Dynamic Middlewares to routes they can either be a single function or multiple functions in an array.
You can also add a mix with both dynamic and global middlewares
They are added before the handler in .app, .get, .post, etc, or using the key
middlewaresif using the .add methodDue to global middlewares being before the handler if you want to add a global middleware by calling get/post/etc then the first parameter CANNOT be a string. That will be interpreted as a route
const { App, Loggur } = require( 'event_request' );
const router = App().Router();
router.define( 'test', ( event )=>{
Loggur.log( 'Middleware One!' );
event.next();
});
// Dynamic middleware
const setHeader = ( key, value )=>{
return ( event )=>{
event.setResponseHeader( key, value );
}
}
App().define( 'test2', ( event )=>{
Loggur.log( 'Middleware Two!' );
event.next();
});
//this will work
App().get( ['test','test2'], ( event )=>{
event.send( 'TEST' );
});
//this will NOT work !!!
App().get( 'test', ( event )=>{
event.send( 'TEST' );
});
//this will work !!!
App().get( ['test'], ( event )=>{
event.send( 'TEST' );
});
//this will work !!!
App().get( setHeader( 'keyOne', 'valueOne' ), ( event )=>{
event.send( 'TEST' );
});
//this will work !!!
App().get( [setHeader( 'keyOne', 'valueOne' ), 'test'], ( event )=>{
event.send( 'TEST' );
});
//this will work !!!
App().get( [setHeader( 'keyOne', 'valueOne' ), setHeader( 'keyTwo', 'valueTwo' )], ( event )=>{
event.send( 'TEST' );
});
App().get( '/', ['test','test2'], ( event )=>{
event.send( 'TEST' );
} );
App().add({
route: '/test',
method: 'GET',
middlewares: 'test',
handler: ( event )=>{
Loggur.log( 'Test!' );
event.send( 'Test2' );
}
});
App().add({
route: '/test',
method: 'GET',
middlewares: ['test'],
handler: ( event )=>{
Loggur.log( 'Test!' );
event.send( 'Test2' );
}
});
App().add({
route: '/test',
method: 'GET',
middlewares: ['test', setHeader( 'value', 'key' )],
handler: ( event )=>{
Loggur.log( 'Test!' );
event.send( 'Test2' );
}
});
App().add( router );
App().listen( 80, ()=>{
Loggur.log( 'Server started' );
});Event Request
The event request is an object that is created by the server and passed through every single middleware.
####Properties of eventRequest:
query: Object
- The query parameters
- Will contain all query parameters in a JS object format
path: String
- The current request path
response: OutgoingMessage
- The response that will be sent to the user
request: IncomingMessage
- The request sent by the user
method: String
- The current request method
- Can be: GET, POST, DELETE, PUT, PATCH, COPY, HEAD, etc
headers: Object
- The current headers
- They will be set in a JS object format
validation: ValidationHandler
- An object used to do input validation
- Look down for more information on how to use it
extra: Object
- An object that holds extra data that is passed between middlewares
- Usually used outside of plugins and native functions and is up to the client to implement if needed
cookies: Object
- The current request cookies
params: Object
- Request url params that are set by the router
- In the case of route: '/user/:username:/get' the parameters will look like this: { username: 'some.username' }
block: Array
- The execution block of middlewares
- Should not be touched usually
clientIp: String
- The ip of the client that sent the request
finished: Boolean
- Flag depicting if the request has finished or not
errorHandler: ErrorHandler
- Default or Custom error handler that will be called in case of an error
####Functions exported by the event request:
setCookie( String name, String value, Object options = {} ): Boolean
- Sets a new cookie with the given name and value
- Options will all be applied directly as are given.
- The options are case sensitive
- Available options: Path, Domain, Max-Age, Expires, HttpOnly
- Expires and Max-Age will be converted to date, so a timestamp in seconds must be passed
- If you wish to expire a cookie set Expires / Max-Age to a negative number
- { Path: 'test', expires: 100 } -> this will be set as 'cookieName=cookieValue; Path:test; expires:100'
setStatusCode( Number code ): void
- Sets the status code of the response
- If something other than a string is given, the status code will be assumed 500
_cleanUp(): void
- Cleans up the event request.
- Usually called at the end of the request.
- Emits a cleanUp event and a finished event.
- This also removes all other event listeners and sets all the properties to undefined
send( mixed response = '', Number statusCode = 200, Boolean raw ): void
- Sends the response to the user with the specified statusCode
- If response is a stream then the stream will be piped to the response
- if the raw flag is set to true then the payload will not be checked and just force sent, otherwise the payload must be a string or if it is not a sting it will be JSON stringified.
- Emits a 'send' event and calls cleanUp
- The event will be emitted with a response if the response was a string or the isRaw flag was set to false
setResponseHeader( String key, mixed value ): void
- Sets a new header to the response.
- Emits a 'setResponseHeader' event.
- If the response is finished then an error will be set to the next middleware
removeResponseHeader( String key ): void
- Removes an existing header from to the response.
- Emits a 'removeResponseHeader' event.
- If the response is finished then an error will be set to the next middleware
redirect( String redirectUrl, Number statusCode = 302 ): void
- Redirect to the given url with the specified status code.
- Status code defaults to 302.
- Emits a 'redirect' event.
- If the response is finished then an error will be set to the next middleware
getRequestHeader( String key, mixed defaultValue = null ): mixed
- Retrieves a header ( if exists ) from the request.
- If it doesn't exist the defaultValue will be taken
hasRequestHeader( String key ): Boolean
- Checks if a header exists in the request
isFinished(): Boolean
- Checks if the response is finished
- A response is finished if the response object returns true when calling isFinished or the cleanUp method has been called
next( mixed err = undefined, Number code = undefined ): void
- Calls the next middleware in the execution block.
- If there is nothing else to send and the response has not been sent YET, then send a server error with a status of 404
- If the event is stopped and the response has not been set then send a server error with a status of 500
- If err !== undefined send an error
sendError( mixed error = '', Number code = 500 ): void
- Like send but used to send errors.
- It will call the errorHandler directly with all the arguments specified ( in case of a custom error handler, you can send extra parameters with the first one being the EventRequest )
validate( ...args ): ValidationResult
- Shorthand for event.validation.validate
- This function will pass any arguments passed to the event.validation.validate function
####Events emitted by the EventRequest
cleanUp()
- Emitted when the event request is about to begin the cleanUp phase.
- At this point the data set in the EventRequest has not been cleaned up
finished()
- Emitted when even cleaning up has finished and the eventRequest is completed
- At this point the data set in the EventRequest has been cleaned up
send( Object sendData )
Emitted when a response has been sent.
sendData contains:
code: Number
- The status code returned
raw: Boolean
- Whether the response was tried to be sent raw without parsing it to string first
response: mixed
- The response that was returned
- Will not be sent if isRaw is true and the response was not a string
headers: Object
- The headers that were sent
setResponseHeader( Object headerData )
- Emitted when a new header was added to the response
- headerData contains:
key: String
- The header name
value: mixed
- The header value
removeResponseHeader( Object headerData )
- Emitted when a header was removed
- headerData contains:
- key: String
- The header name
- key: String
redirect( Object redirectData )
Emitted when a redirect response was sent
redirectData contains:
redirectUrl: String
- the url to which the redirect response was sent
statusCode: String
- the status code returned
Server
The main object of the framework.
- To retrieve the Server class do:
const { Server } = require( 'event_request' );
const app = Server();- To start the Server you can do:
const { App, Loggur } = require( 'event_request' );
const app = App();
app.listen( '80', ()=>{
Loggur.log( 'Server is running' );
});- To clean up the server instance you can do:
const { App, Loggur } = require( 'event_request' );
const app = App();
const httpServer = app.listen( '80', ()=>{
Loggur.log( 'Server is running' );
});
httpServer.close();
App().cleanUp();NOTES:
This will stop the httpServer and set the internal variable of server to null
You may need to do
app = App()again since they app variable is still a pointer to the old serverIf you want to start the server using your own http/https server:
const http = require( 'http' );
const { App, Loggur } = require( 'event_request' );
const app = http.createServer( App().attach() );
// No problem adding routes like this
App().get( '/',( event )=>{
event.send( 'ok' );
});
// OR like this
app.get( '/test',( event )=>{
event.send( 'ok' );
});
app.listen( '80',()=>{
Loggur.log( 'Server is UN' )
});- Calling
App()anywhere will return the same instance of the Framework.
####Functions exported by the server:
getPluginManager(): PluginManager
- Returns an instance of the plugin manager attached to the server
add( Object|Route|Function route ): Server
- Calls Router.add
apply( PluginInterface|String plugin, Object options ): Server
- Applies a new plugin with the specified options
- It first calls setOptions, then checks for dependencies, then calls plugin.setServerOnRuntime then calls plugin.getPluginMiddleware
getPlugin( String|PluginInterface pluginId ): PluginInterface
- PluginInterface returns the desired plugin
- Throws if plugin is not attached
hasPlugin( String|PluginInterface pluginId ): Boolean
- Checks whether a plugin has been added to the server.
- This does not work with the plugin manager but the server's plugins
define( String middlewareName, Function Middleware ): Server
- Calls Router.define
Router(): Router
- Returns a new Router instance that can be used anywhere and later on add() -ed back to the server
attach(): Function
- Returns the middleware needed by http.createServer or https.createServer
listen( ... args )
- Starts a http server.
- Any arguments given will be applied to httpServer.listen
####Events emitted by the server
addRoute ( mixed route )
- When a new route is being added
eventRequestResolved ( EventRequest eventRequest, IncomingMessage request, ServerResponse response )
- When the event request is first created
eventRequestRequestClosed( EventRequest eventRequest, IncomingMessage request )
- When the request gets closed
eventRequestResponseFinish( EventRequest eventRequest, ServerResponse response )
- When the response is finished
eventRequestResponseError ( EventRequest eventRequest, ServerResponse response, Error error )
- When there is an error with the response
eventRequestBlockSetting( EventRequest eventRequest, Array block )
- called when the block is retrieved from the router
eventRequestBlockSet( EventRequest eventRequest, Array block )
- called when the block is set in the eventRequest
eventRequestError( EventRequest eventRequest, Error error )
- called when there is an error event emitted by the eventRequest
eventRequestThrow( EventRequest eventRequest, Error error )
- called when an error is thrown from the eventRequest
Logging
The Logging Suite exported by the module contains the following:
- Loggur -> instance of Loggur used to log data and create Loggers
- Logger -> The Logger class
- Transport -> The interface used by the loggers
- Console -> Transport that logs to the console
- File -> Transport that logs to a file
- Log -> The Log object used by all the internal classes
- LOG_LEVELS -> The Default log levels
- The Loggur can be accessed directly from the server { Loggur }
Default Logger:
The default logger is attached directly to the Loggur instance. it can be enabled or disabled by calling Loggur.enableDefault() or Loggur.disableDefault().
The default Logger has a log level of
300and logs up until level600which is the debug level.The Loggur can create Loggers with Loggur.createLogger({});
####Loggur.createLogger options:
serverName: String
- The name of the server to be concatenated with the uniqueId
- Defaults to empty
transports: Array
- Array of the transports to be added to the logger
- Defaults to empty
logLevel: Number
- The log level lower than which everything will be logged
- This will also be the default logLevel for the logger
- Example: if the logLevel is set to LOG_LEVELS.info then info, notice, warning and error will be logged, but verbose and debug will not
- The higher a log level is the less sever it is
- Defaults to LOG_LEVELS.info
logLevels: Object
- JSON object with all the log severity levels and their values All added log levels will be attached to the instance of the logger class
- The logger will be able to log ONLY on these log levels
- If you have log levels: 100 and 200 and you try with a log level of 50 or a 300 it won't log
- Defaults to LOG_LEVELS
capture: Boolean
- Whether to attach event listeners for process.on uncaughtException and unhandledRejection
- Defaults to false
dieOnCapture: Boolean
- If the process should exit in case of a caught exception
- Defaults to true
unhandledExceptionLevel: Number
- What level should the unhandled exceptions be logged at
- Defaults to error
- If you want to change the log level of a logger it can easily be done with .setLogLevel( logLevel )
logger.setLogLevel( 600 );Loggers can be added to the main instance of the Loggur who later can be used by: Loggur.log and will call all added Loggers
const { Loggur } = require( 'event_request' );
const logger = Loggur.createLogger({
transports : [
new Console( { logLevel : LOG_LEVELS.notice } ),
]
});
Loggur.addLogger( 'logger_id', logger );####Logging:
#####Logger.log:
- Every logger has a log function that supports the following parameters:
Log: Log||String||mixed
- What should be logged
level: Number
- The log level that we should log at
- optional
- Defaults to the default logLevel of the logger
isRaw: Boolean
- Whether we should attempt to log the data raw
- Only specific transport types support raw
- Defaults to false
logger.log( 'Log' ); // This logs by default to an error level
logger.log( 'Log', LOG_LEVELS.debug ); // LOG_LEVELS.debug === Number, this will log 'Log' with debug level
logger.log( { test: 'value' }, LOG_LEVELS.debug, true ); // This will log on debug and will try to log the data raw#####Loggur.log:
- The main Loggur has a log function that calls all the loggers added to it
- If there are no loggers it will call it's default logger if enabled
Log: Log||String||mixed
- What should be logged
level: Number
- The log level that we should log at
- optional
- Defaults to the default logLevel of the logger
isRaw: Boolean
- Whether we should attempt to log the data raw
- Only specific transport types support raw
- Defaults to false
Loggur.log( 'Log' ); // This logs by default to an error level
Loggur.log( 'Log', LOG_LEVELS.debug ); // LOG_LEVELS.debug === Number, this will log 'Log' with debug level
Loggur.log( { test: 'value' }, LOG_LEVELS.debug, true ); // This will log on debug and will try to log the data raw#####logger.error || logger.debug etc:
- Every logger attaches all the give log levels as functions that accept the following arguments:
Log: Log||String||mixed
- What should be logged
isRaw: Boolean
- Whether we should attempt to log the data raw
- Only specific transport types support raw
- Defaults to false
logger.error( 'Log' ); // This logs by default to an error level
logger.debug( 'Log', true ); // This will log on debug and will try to log the data rawEach Logger can have it's own transport layers. There are 2 predefined transport layers:
###Console
- Logs data in the console
- It can log raw logs
####Accepted options:
logLevel: Number
- The log level lower than which everything will be logged
- This will also be the default logLevel for the logger
- Example: if the logLevel is set to LOG_LEVELS.info then info, notice, warning and error will be logged, but verbose and debug will not
- The higher a log level is the less sever it is
- Defaults to LOG_LEVELS.info
logLevels: Array
- JSON object with all the log severity levels and their values All added log levels will be attached to the instance of the logger class
- The logger will be able to log ONLY on these log levels
- If you have log levels: 100 and 200 and you try with a log level of 50 or a 300 it won't log
- Defaults to LOG_LEVELS
color: Boolean
- Whether the log should be colored
- Defaults to true
logColors: Object
- The colors to use
- Defaults to
- [LOG_LEVELS.error] : 'red',
- [LOG_LEVELS.warning] : 'yellow',
- [LOG_LEVELS.notice] : 'green',
- [LOG_LEVELS.info] : 'blue',
- [LOG_LEVELS.verbose] : 'cyan',
- [LOG_LEVELS.debug] : 'white'
###File
- Logs data to a file
- It can't log raw logs
####Accepted options:
logLevel: Number
- The log level lower than which everything will be logged
- This will also be the default logLevel for the logger
- Example: if the logLevel is set to LOG_LEVELS.info then info, notice, warning and error will be logged, but verbose and debug will not
- The higher a log level is the less sever it is
- Defaults to LOG_LEVELS.info
logLevels: Array
- JSON object with all the log severity levels and their values All added log levels will be attached to the instance of the logger class
- The logger will be able to log ONLY on these log levels
- If you have log levels: 100 and 200 and you try with a log level of 50 or a 300 it won't log
- Defaults to LOG_LEVELS
filePath: String
- The location of the file to log to
- If it is not provided the transport will not log
const { Logging } = require( 'event_request' );
const { Loggur, LOG_LEVELS, Console, File } = Logging;
// Create a custom Logger
const logger = Loggur.createLogger({
serverName : 'Test', // The name of the logger
logLevel : LOG_LEVELS.debug, // The logLevel for which the logger should be fired
capture : false, // Do not capture thrown errors
transports : [
new Console( { logLevel : LOG_LEVELS.notice } ), // Console logger that logs everything below notice
new File({ // File logger
logLevel : LOG_LEVELS.notice, // Logs everything below notice
filePath : '/logs/access.log', // Log to this place ( this is calculated from the root folder ( where index.js is )
logLevels : { notice : LOG_LEVELS.notice } // The Log levels that this logger can only log to ( it will only log if the message to be logged is AT notice level )
}),
new File({
logLevel : LOG_LEVELS.error,
filePath : '/logs/error_log.log',
}),
new File({
logLevel : LOG_LEVELS.debug,
filePath : '/logs/debug_log.log'
})
]
});Default log levels:
- error : 100,
- warning : 200,
- notice : 300,
- info : 400,
- verbose : 500,
- debug : 600
Validation
The validation is done by using:
const objectToValidate = { username: 'user@test.com', password: 'pass' };
event.validation.validate(
objectToValidate,
{
username: 'filled||string||email||max:255',
password: 'filled||string||range:6-255'
}
);skeleton must have the keys that are to be validated that point to a string of rules separated by ||
if the object to be validated is multi dimensional then you can specify the multi dimensional keys as well ( see example below )
Asserting that an input is a string|numeric|boolean will do type conversion on the input to the appropriate type. You should retrieve the value FROM the validation result for correct result
Possible rules are:
optional
- if set as long as the input is empty it will always be valid. if not empty other possible rules will be called
filled
- checks if the input is filled
array
- checks if the input is an array
notArray
- checks if the input is not an array
string
- checks if the input is a string.
- Will convert a number to a string
weakString
- checks if the input is a string
- will not do value conversion
notString
- checks if the input is NOT a string
range
- Is followed by min and max aka: range:1-2 where 1 is the minimum and 2 maximum.
- If the type is numeric then it will check if it's within the range.
- If the type is a string then the length will be checked
- If the type is an array then the array length will be checked
min
- minimum input length aka: min:10
- Same rules about types apply as in range
max
- maximum input length aka: max:50
- Same rules about types apply as in range
- checks if the input is a valid email
isTrue
- checks if the input evaluates to true
- 1 will be asserted as true, true will be asserted as true, 'true' and '1' will also be asserted as true
- The value will be converted to a boolean
weakIsTrue
- checks if the input equals to true ( boolean )
- will not do value conversion
isFalse
- checks if the input evaluates to false
- 0 will be asserted as false, false will be asserted as false, 'false' and '0' will also be asserted as false
- The value will be converted to a boolean
weakIsFalse
- checks if the input equals to false ( boolean )
- will not do value conversion
boolean
- checks if the input is a boolean
- The value will be converted to a boolean
- Applies the same rules as isFalse and isTrue
weakBoolean
- checks if the input is a boolean
- will not do value conversion
notBoolean
- checks if the input is not a boolean
numeric
- checks if the input is a number.
- This will convert the input to a Number if possible
weakNumeric
- checks if the input is numeric
- will not do value conversion
notNumeric
- checks if the input is not a number
date
- checks if the input is a date
same
- checks if the input is the same as another input aka: same:emailInput
different
- checks if the input is different from another input aka: different:emailInput
equals
- checks if the input equals another given string: equals:makeSureToEqualToThis
####Note: in case of an error the exact rule/s that failed will be returned, furthermore if the rules passed were malformed a special "rules" error will be returned for the field/s
When validation is done a ValidationResult is returned. It has 2 methods: getValidationResult that in case of a validation error will return an object with the fields tested mapped to the errors found. Otherwise it will be an object with the fields tested mapped to the values hasValidationFailed that returns a boolean whether there is an error
const result = event.validation.validate(
event.body,
{
username : 'filled||string||range:6-32',
password : 'filled||string',
customerNumber: 'numeric||max:32',
address: { street: 'string', number: 'numeric' },
extra : { $rules: 'optional||string', $default: 'def' }
}
);
console.log( result.hasValidationFailed() );
console.log( result.getValidationResult() );
// If errors were found hasValidationFailed would return true and getValidationResult will have a map
// of which input failed for whatever reason. Otherwise getValidationResult will return an object :
// { 'username':'username', 'password': 'password', 'customerNumber': 16, address: { street: 'street', number: 64 }, extra: 'def' }The example will validate that the username is filled is a string and is within a range of 6-32 characters It will also validate that the password is filled and is a string The customerNumber will be validated to be numeric that is lower than 32 The address will be asserted to be an object and the keys in it will be validated The extra if not passed will default to 'def'
####Validation defaults
- Validation results can also have defaults set.
- This is done by instead of passing a string of rules to the skeleton keys, an object is passed with two values: $rules and $default
- The $rules must be optional otherwise validation will fail
- In case where the parameters have NOT been passed, the default value will be used.
const result = event.validation.validate(
event.body,
{
username : { $rules: 'optional||string', $default: 'root' },
password : { $rules: 'optional||string', $default: 'toor' }
}
);
console.log( result.hasValidationFailed() );
console.log( result.getValidationResult() );
// If errors were found hasValidationFailed would return true and getValidationResult will have a map
// of which input failed for whatever reason. Otherwise getValidationResult will return an object :
// { 'username':'username', 'password': 'password'}const dataToValidate = {
testOne : 123,
testTwo : '123',
123 : [1,2,3,4,5],
testThree : {
'deepOne' : 123,
deepTwo : {
deeperOne : 123,
deeperTwo : '123'
}
},
testFour : true,
testFive : 'true',
testSix : '1',
};
const result = event.validation.validate(
dataToValidate,
{
testOne : 'string||range:2-4',
testTwo : 'numeric||range:123-124',
123 : 'array||range:4-6',
testThree : {
deepOne : 'numeric||range:122-124',
deepTwo : {
deeperOne : 'string||range:2-4',
deeperTwo : 'numeric||range:123-124'
}
},
testFour : 'boolean',
testFive : 'boolean',
testSix : 'boolean'
}
);
console.log( result.hasValidationFailed() ); // false
console.log( result.getValidationResult() );
//{
// '123' : [1,2,3,4,5],
// testOne : '123',
// testTwo : 123,
// testThree : {
// deepOne : 123,
// deepTwo : {
// deeperOne : '123',
// deeperTwo : 123
// }
// },
// testFour : true,
// testFive : true,
// testSix : true,
//}LeakyBucket
This class can be used to limit data in one way or another.
####Accepted constructor arguments:
refillAmount: Number
- How many tokens to refill after the refillTime
- Defaults to 100
refillTime: Number
- How long after we should refill in seconds
- If 1 is passed and 2 seconds pass, we will refill refillAmount * 2
- Defaults to 60
maxAmount: Number
- The max amount of tokens to be kept
- Defaults to 1000
prefix: String
- Prefix that the data will be stored under in the DataStore provided
- Defaults to $LB:
key: String|null
- The current key that the bucket is stored under
- If this is provided the bucket settings will be retrieved from the dataStore using this key without adding a prefix or generating a new one
- Defaults to null ( generate a random 64 chars key and add a prefix )
dataStore: DataServer
- Instance of a DataServer to use for storage
- By default uses the in memory one with persistency set to false and ttl set to: this.maxAmount / this.refillAmount * this.refillTime * 2
dataStoreRefetchInterval: Number
- Milliseconds after which a retry should be sent to the dataStore ( usually should be set to 1 or 2, set to more if the dataStore cannot handle a lot of traffic )
- Used to set the maxCounter using the following formula: Math.max( Math.floor( 1000 / dataStoreRefetchInterval ), 1 )
- Defaults to 1
####The class has the following functions:
async init(): void
- This has to be called before using the class
async reset(): void
- Resets the tokens to full
async get(): Number
- Returns the currently available tokens
async reduce( tokens = 1 ): Boolean
- How many tokens should be taken.
- This function returns Boolean whether there were enough tokens to be reduced or not
async isFull(): Boolean
- This function returns Boolean whether the bucket is full
####Example:
const LeakyBucket = require( 'event_request/server/components/rate_limiter/bucket' );Testing
If you need to test your project, then you can use the Testing tools included in the project.
const { Testing } = require( 'event_request' );Accepted CLI arguments
--filter=
- Accepts a string to filter by
- Example: node test.js --filter=DataServer
--silent
- Silences the errors
- Example: node test.js --silent
--debug
- Sets it to debug
- Example: node test.js --debug
--dieOnFirstError=
- Accepts 1 or 0 whether the tester should die on first error
- Example: node test.js --dieOnFirstError=1
- Example: node test.js --dieOnFirstError=0
Notes:
The testing tools include a mocker. The mocker class can be retrieved with:
const { Mock } = Testing;The exported Mock is a Function that should be used directly on the constructor of the class you want to mock. For example:
class Test { mockThis(){} };
const MockedTest = Mock( Test ); This will return the same class but with an extra _mock function added directly to it so make sure your original class does NOT have a _mock function otherwise it will be overwritten. From here you can use the _mock function to mock any other function/parameter that is attached to the 'Test' class:
const testDouble = new MockedTest();
testDouble._mock({
method : 'mockThis',
shouldReturn : ''
}); Note: As you can see when you mock a class you MUST specify what it should return from now on. You can also give instructions on what should be returned on consecutive calls to this method like so :
const testDouble = new MockedTest();
testDouble._mock({
method : 'mockThis',
onConsecutiveCalls : ['first', 'secondAndOnwards']
});This will result in the following:
- The first time you make a call to mockThis you will get 'first' as a return
- The second time you make a call to mockThis you will get 'secondAndOnwards' as a return
- Third time you make a call and any other following you will also get 'secondAndOnwards'
When making a mock of a class you can specify the MAX amount of times an object should be called. Since javascript uses an async approach and relies heavily on callbacks, a minimum cannot be set.
const testDouble = new MockedTest();
testDouble._mock({
method : 'mockThis',
shouldReturn : '',
called : 1
});This way if the method mockThis is called more than once an error will be thrown.
You can also Specify the arguments that should be provided to the mocked method like so:
const testDouble = new MockedTest();
testDouble._mock({
method : 'mockThis',
shouldReturn : '',
called : 1,
with: [
[ 'firstArgument', 'secondArgument' ],
[ 'secondCallFirstArgument', 'secondCallSecondArgument' ],
[ 'iWantToCheckThis', undefined ],
[ undefined, 'iWantToCheckThis' ]
]
}); The 'with' option accepts an array of arrays where each array in the with array is a call. Again if it's called more than the times the with arguments, the last one will be returned. In case of mismatch an Error will be thrown. If you do not want the mocker to check one of the arguments, then undefined should be passed
If you wan an environment to run your tests then you can use the test and runAllTests provided by the testing tools:
const { test, runAllTests } = TestingTools;The 'runAllTests' function accepts an object that accepts the following options:
dieOnFirstError: Boolean
- Whether the testing should stop on the first error
- Defaults to true
debug: Boolean
- Whether errors thrown should show their entire stack or just the message
- Defaults to false
silent: Boolean
- This will set the consoleLogger logLevel to error, meaning only errors will be displayed
- Defaults to false
filter: String
- the string to search for and filter by when testing
- Defaults to false
callback: Function
- Callback to be called when testing is complete
- The run all tests will run all tests added by the test function.
- If there is an err or an Error is thrown then the process with exit with code 1 otherwise it will exit with code 0
###The 'test' function accepts an object with the following options:
message: String
- the name of the test
skipped: Boolean
- defaults to false
- If this is set to true the test will be skipped
incomplete: Boolean
- defaults to false
- If this is set to true the test will be marked as incomplete
dataProvider: Array
- Optional
- If this is provided then an Array of Arrays must be supplied.
- For each Array supplied, a new test will be created and called with the Array elements set as arguments to the test callback
test: Function
- the callback to execute.
- the tester provides a done function as the first argument to the test callback.
- The done should be called just ONCE and only when the test finishes.
- If done is called twice within the same test then that will be seen as an error and the testing will stop.
- If any arguments that evaluate to true are provided to done then the test will be seen as failed.
test({
message : 'This test should pass',
dataProvier : [
['first', 2 ],
['firstTwo', 21 ],
],
test : ( done, first, second ) =>{
console.log( first ); //this will log 'first', then on the second iterration 'firstTwo'
console.log( second ); //this will log 2, then on the second iterration 21
let one = 1;
one === 1 ? done() : done( 'One does not equal to one what are you doing?!' );
}
}); - You can also create your own Tester if you want separate test cases:
const { Tester } = TestingTools;
let tester = new Tester(); - The tester has the same functions: 'test', 'runAllTests'
###Mocker You can also use the Mocker class by:
Mocker( classToMock, methodToMockOptions )- The methodToMockOptions are the same as the _mock function of a testDouble.
- Note that this can alter a class before it is actually instantiated and WILL alter the original class passed so it is suggested to be used ONLY on testDoubles
The TestingTools export:
- Tester, -> Tester constructor
- Mock, -> Mock function
- Mocker, -> the class used to mock methods of testDoubles. Please note that if you use this class you will alter the original one
- assert, -> nodejs assert module
- tester, -> Already created tester
- test : tester.addTest.bind( tester ),
- runAllTests : tester.runAllTests.bind( tester )
#ErrorHandler
- There is a default error handler in the event request
- This class can be extended and custom functionality may be written
- Alternatively instead of extending you can write your own class as long as it has a handleError function.
####Accepted Options:
NONE
####Events:
on_error: ( mixed error )
- The error returned will be the one returned from ::_getErrorToEmit()
####Functions:
handleError( EventRequest event, Error error, Number code = 500 ): void
- Emits an on_error event
- Calls _sendError
- In case of extension of the ErrorHandler, this may not need to be touched
_getErrorToEmit( Error error ): mixed
- Returns error to be emitted
- By default if error instanceof Error only the error.stack will be returned
- In case of extension of the ErrorHandler this function can be overwritten
_formatError( Error error ): mixed
- Returns error to be sent
- By default if error instanceof Error only the error.message will be returned
- In case of extension of the ErrorHandler this function can be overwritten
_sendError( Error error ): mixed
- Sends the error to the user
- Will not send if the response is finished
- In case of extension of the ErrorHandler this function can be overwritten
####Attached Functionality:
event.errorHandler: ErrorHandler
- By default it is attached to the EventRequest but can be overwritten at any point
####Example:
const Server = require( 'event_request' );
const app = Server();
app.add(( event )=>{
event.sendError( 'Error', 500 ); // This will call the error Handler
event.next( 'Error', 500 ); // This will call the error Handler
event.send( 'Error', 500 ); // This will !!NOT!! call the error Handler
});
app.listen( 80 );#BodyParser
- If you want to create a new BodyParser the new BodyParser must implement the functions described below
Accepted options
NONE
Functions
supports( EventRequest event ): Boolean
- This function will be called by the BodyParserHandler attached by the body parser plugin before the parser is actually called
- It must return a Boolean
- If a parser returns that it supports the given request, no further body parsers will be called
parse( EventRequest event ): Promise: body
- Returns a promise
- This is called only if the body parser is supported.
- It resolves with a body that is then attached to event.body or rejects with an error
Examples
- If you want to add a custom BodyParser you can do:
const BodyParserPlugin = require( 'event_request/server/plugins/available_plugins/body_parser_plugin' );
class CustomBodyParser
{
parse(){} // define logic
supports(){} // define logic
}
// The CustomBodyParser is the class and the options are the end are the parameters to be passed to the class
// This is done because A new body parser will be created on each request
const plugin = new BodyParserPlugin( CustomBodyParser, 'custom_body_parser', { optionOne: 123, optionTwo: 'value' } );DataServer
- DataServer is a class that is exported through the Server.Development suite that stores data IN MEMORY
- Is an EventEmitter
- Can be extended
const DataServer = require( 'event_request/server/components/caching/data_server' );
console.log( DataServer );
console.log( new DataServer( options ) );Accepted options
ttl: Number
- The time in seconds to be used as a default 'Time To Live' if none is specified.
- If ttl is set to -1 then the data will never expire
- Defaults to 300
persistPath: String
- The absolute path of the file that will persist data.
- Defaults to
/cache
persistInterval: Number
- The time in seconds after which data will be persisted.
- Defaults to 100
gcInterval: Number
- The time in seconds after which data will be garbageCollected.
- Defaults to 60
persist: Boolean
- Flag that specifies whether the data should be persisted to disk.
- Defaults to true
The DataServer provides a set of methods that have to be implemented if you want to create your own Caching server to be integrated with other plugins.
Events:
_saveDataError( Error error )
- Emitted in case of an error while saving data
_saveData()
- Emitted when the data has finished saving
stop()
- Emitted when the server is stopping
touch( { String key, Number ttl, Object options } ) get( { String key, Object options } ) lock( { String key, Object options } ) unlock( { String key, Object options } ) delete( { String key, Object options } ) set( { String key, mixed value, Number ttl, Object options } ) increment( { String key, mixed value, Object options } ) decrement( { String key, mixed value, Object options } )
Functions:
stop(): void
- This will stop the connection of the DataServer
- It calls _stop()
- It emits a 'stop' event
- It clears all the intervals
- It removes all the listeners
_stop(): void
- This method is the protected method that should be implemented in case extension of the DataServer should be done
- Removes the cache file
_setUpPersistence(): void
- This method is the protected method that should be implemented in case extension of the DataServer should be done
- It is called in the constructor to create the cache file we will be using if persistence is enabled
get( String key, Object options ): Promise: Object|null
- Retrieves the value given a key. Returns null if the key does not exist.
- This function is a 'public' method to be used by users.
- In the case that you want to implement your own DataServer, you should override _get( String key )
- Emits a get event
_get( String key, Object options ): Promise: mixed|null
- This method is the protected method that should be implemented in case extension of the DataServer should be done
- Removes the DataSet if it is expired, otherwise returns it. Returns null if the data is removed.
- No need to check if key is a String, that has been done in the _get method already.
- This method also sets the expiration of the DataSet to Infinity if it is null.
- This will return the value set by set()
set( String key, mixed value, Number ttl = 0, Object options ): Promise: Object|null
- Returns the data if it was set, otherwise returns null
- Sets the given key with the given value.
- ttl is the time in seconds that the data will be kept.
- If ttl is -1 then the dataSet will NEVER expire
- If ttl is 0 then the Default TTL will be used.
- If ttl is > 0 then the value will be used
- Calls _set() after checking the arguments if they are valid
- Emits a set event
_set( String key, mixed value, Number ttl = 0, Object options ): Promise: Object|null
- Implement for development. No need to do checks of the values of the parameter as that is done in the set() function
- This function commits the key/value to memory with all it's attributes
- If the dataSet existed, then a key 'isNew' must be set to true or false
- The options accept a Boolean flag persist that will override the global persist value. You can set a key to not be persisted. However if the global persist is set to false, this will not work
- Returns the data if it was set, otherwise returns null
_makeDataSet( String key, mixed value, Number ttl, Boolean persist ): Object
- Forms the dataSet object and returns it in the following format:
{ key, value, ttl, expirationDate, persist };
touch( String key, Number ttl = 0, Object options ): Promise: Boolean
- Returns a Boolean whether the data was successfully touched
- Returns a false if key is not String or ttl is not Number
- Calls _touch after checking if arguments are valid
- Emits a touch event
_touch( String key, Number ttl = 0, Object options ): Promise: Boolean
- Implement for development. No need to do checks of the values of the parameter as that is done in the touch() function
- Returns a Boolean whether the data was successfully touched
- If ttl = 0 then the dataSet will be updated with it's own ttl
- This function actually touches the data
decrement( String key, Number value = 1, Object options ): Promise: Boolean
- If value is not a number, returns false
- If the data was not set correctly returns false
- If the data to decrement was not set correctly returns false
- If the data to decrement was not numeric returns false
- Calls _decrement() after checking for validity of data
- Emits a decrement event
- The ttl of the value will be extended by it's original ttl
_decrement( String key, Number value = 1, Object options ): Promise: Boolean
- Implement for development. No need to do checks of the values of the parameter as that is done in the decrement() function
- Retrieves, decrements and then saves the new dataset
- If the operation is successfully done, returns true
increment( String key, Number value = 1, Object options ): Promise: Boolean
- If value is not a number, returns false
- If the data was not set correctly returns false
- If the data to increment was not set correctly returns false
- If the data to increment was not numeric returns false
- Calls _increment() after checking for validity of data
- Emits an increment event
- The ttl of the value will be extended by it's original ttl
_increment( String key, Number value = 1, Object options ): Promise: Boolean
- Implement for development. No need to do checks of the values of the parameter as that is done in the increment() function
- Retrieves, increment and then saves the new dataset
- If the operation is successfully done, returns true
delete( String key, Object options ): Promise: Boolean
- Deletes the given data
- Emits a delete event
- WIll return false if arguments are invalid
_delete( String key, Object options ): Promise: Boolean
- Implement for development. No need to do checks of the values of the parameter as that is done in the delete() function
- This function deletes the actual data
- Will return true always
lock( String key, Object options ): Promise: Boolean
- Acquires a lock given a key.
- Emits a lock event
- This calls _lock
_lock( String key, Object options ): Promise: Boolean
- Implement for development. No need to do checks of the values of the parameter as that is done in the lock() function
- Acquires a lock given a key.
- This will return true only if there is no key like that in the DataServer, otherwise return false
unlock( String key, Object options ): Promise: Boolean
- Releases a lock
- Emits a unlock event
- This calls _unlock
_unlock( String key, Object options ): Promise: Boolean
- Implement for development. No need to do checks of the values of the parameter as that is done in the unlock() function
- Releases a lock
- This returns true always
_garbageCollect(): void
- Prunes all the data from the server if needed
- Implement this if your Data Server needs it, otherwise leave it blank
_saveData(): void
- Persists all the data set to be persisted to disk
- Extra measures have been taken so this operation will not break if it is running fast, however if the persist interval is too low it still may cause an issue while saving
- This respects any data set with persist = false
_loadData(): void
- Loads all the data from disk
_getExpirationDateFromTtl( Number ttl = -1 ): Number
- Gets the the correct ttl according to the rules described in set()
Used for development purposes:
length(): Number
- Returns how many keys there are
Plugins
- Plugins can be added by using server.apply( PluginInterfaceObject ||'pluginId', options )
- Plugins can be added to the server.pluginManager and configured. Later on if you want to apply the preconfigured plugin all you have to do is do: server.apply( 'pluginId' )
- To enable IDE's smart autocomplete to work in your favor all the plugins available in the pluginManager are exported as values in the server:
- The plugin interface can be retrieved like so:
const PluginInterface = require( 'event_request/server/plugins/plugin_interface' );Server {
...
er_timeout,
er_env,
er_rate_limits,
er_static_resources,
er_data_server,
er_templating_engine,
er_file_stream,
er_logger,
er_session,
er_security,
er_cors,
er_response_cache,
er_body_parser_json,
er_body_parser_form,
er_body_parser_multipar,
er_body_parser_raw,
er_validation,
}- Generally all the integrated plug-ins begin with
er_
const Server = require( 'event_request' );
const app = Server();
const PluginManager = app.getPluginManager();
const timeoutPlugin = PluginManager.getPlugin( 'er_timeout' );
timeoutPlugin.setOptions( { timeout : 10 * 1000 } );
app.apply( timeoutPlugin );
app.apply( timeoutPlugin, { timeout : 10 * 1000 } );// This will accomplish the same thing as the rows above
app.apply( 'er_timeout' ); // This is also valid.
app.apply( 'er_timeout', { timeout : 10 * 1000 } ); // This is also valid.
app.apply( app.er_timeout ); // This is also valid.
app.apply( app.er_timeout, { timeout : 10 * 1000 } ); // This is also valid.PluginInterface
The PluginInterface has a getPluginMiddleware method that must return an array of middleware objects implementing handler, route, method keys or instances of Route.
The PluginInterface has a setOptions function that can be used to give instructions to the Plugin when it is being created and added to the event request
The PluginInterface implements a getPluginDependencies method that returns an Array of needed plugins to work. These plugins must be installed before the dependant plugin is.
The PluginInterface implements a setServerOnRuntime method that passes the server as the first and only argument. Here the plugin can interact with the server.pluginBag to store any data it seems fit or may modify the server in one way or another.
Generally plugins should not have any business logic in the constructor and rather have that in the setServerOnRuntime or getPluginMiddleware functions. This is the case because new options can be given to the plugin when attaching to the server.
This is how the flow of adding a plugin goes:
- Check if there are any options passed and if so, apply them with setOptions
- Check if dependencies are matched
- setServerOnRuntime
- getPluginMiddleware
Plugin Manager
The manager can be extracted from the created Server by:
const pluginManager = server.getPluginManager();The Plugin manager contains pre loaded plugins. You can add your own plugins to it for easy control over what is used or if you want the bootstrap of the project to be in a different place.
The plugin Manager exports the following functions:
addPlugin( plugin ) - accepts only a plugin of instance PluginInterface and only if it does not exist already otherwise throws an exception
hasPlugin( id ) - checks if a plugin with the specified id exist
removePlugin( id ) - removes a plugin
getAllPluginIds() - returns an array with all the possible plugins
getPlugin( id ) - returns a PluginInterface otherwise throw
Available plugins:
#er_timeout
- Adds a timeout to the request
####Dependencies:
NONE
####Accepted Options:
timeout
- the amount of milliseconds after which the request should timeout - Defaults to 60 seconds or 60000 milliseconds
####Events:
clearTimeout()
- Emitted when the event.clearTimeout() function is called if there was a timeout to be cleared
####EventRequest Attached Functions
event.clearTimeout(): void
- Clears the Request Timeout
- Will do nothing if there is no timeout
####Attached Functionality:
event.internalTimeout: Timeout
- The request timeout set in the EventRequest
####Exported Plugin Functions:
NONE
####Example:
const Server = require( 'event_request' );
const app = Server();
app.apply( 'er_timeout', { timeout: 10000 } );
// OR
app.apply( 'er_timeout' );
// OR
app.apply( app.er_timeout );
// OR
const PluginManager = app.getPluginManager();
const timeoutPlugin = PluginManager.getPlugin( 'er_timeout' );
timeoutPlugin.setOptions( { timeout : 10000 } ); // 10 seconds
app.apply( timeoutPlugin );#er_static_resources
- Adds a static resources path to the request.
- By default the server has this plugin attached to allow favicon.ico to be sent
- The Content-Type header will be set with a mime type if the file is css or js
####Dependencies:
NONE
####Accepted Options:
paths: Array[String] | String
- The path/s to the static resources to be served. Defaults to 'public'
- Can either be an array of strings or just one string
- The path starts from the root of the project ( where the node command is being executed )
####Events:
NONE
####EventRequest Attached Functions
NONE
####Attached Functionality:
NONE
####Exported Plugin Functions:
NONE
####Example:
const Server = require( 'event_request' );
const app = Server();
app.apply( app.er_static_resources, { paths : ['public'] } );
//OR
app.apply( 'er_static_resources', { paths : ['public'] } );
//OR
app.apply( 'er_static_resources' );
//OR
app.apply( app.er_static_resources );
//OR
const PluginManager = app.getPluginManager();
const staticResourcesPlugin = PluginManager.getPlugin( 'er_static_resources' );
staticResourcesPlugin.setOptions( { paths : ['public', 'favicon.ico'] } );
app.apply( staticResourcesPlugin );#er_data_server
- Adds a Caching Server using the DataServer provided in the constructor if any.
- This plugin will add a DataServer to:
event.cachingServer
####Dependencies:
NONE
####Accepted Options:
dataServerOptions: Object
- The options to be passed to the DataServer if the default one should be used
dataServer: Object
- An already instantiated child of DataServer to be used insted of the default one
####Events:
NONE
####EventRequest Attached Functions
NONE
####Attached Functionality:
event.cachingServer: DataServer
- The caching server will be available to be used within the EventRequest after it has been applied in the middleware block
- You can retrieve the DataServer from any other plugin after this one has been applied by doing: server.getPlugin( 'er_data_server' ).getServer()
####Exported Plugin Functions:
getServer(): DataServer
- Returns the instance of the DataServer, following a singleton pattern
####Example:
- You can add the plugin like:
const Server = require( 'event_request' );
const app = Server();
app.apply( 'er_data_server' );
// OR
app.apply( app.er_data_server );
// OR if you have made a child of the DataServer:
app.apply( app.er_data_server, { dataServer: new CustomDataServer() } );
// OR if you want to pass specific parameters to the default DataServer:
app.apply( app.er_data_server, { dataServerOptions: { persist: false, ttl: 200, persistPath: '/root' } } );- The plugin can be used like:
const { Loggur } = require( 'event_request' );
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
app.apply( app.er_data_server, { persist: false } );
// Add a new Route
app.get( '/', ( event ) => {
event.cachingServer.set( 'key', 'value' );
console.log( event.cachingServer.get( 'key' ) );
event.send( '<h1>Hello World!</h1>' );
});
app.listen( 80, ()=>{
Loggur.log( 'Server started' );
});#er_session
- Adds a Session class.
- The session works with a cookie or a header.
- The cookie/header will be sent back to the client who must then return the cookie/header back.
####Dependencies:
er_data_server
####Accepted Options:
ttl: Number
- Time in seconds the session should be kept.
- Defaults to 90 days or 7776000 seconds
sessionKey: String
- The cookie name.
- Defaults to
sid
sessionIdLength: Number
- The size of the session name.
- Defaults to 32
isCookieSession: Boolean
- Flag that determines if the session is in a cookie or a header
- Defaults to true ( session cookie )
####Events:
NONE
####EventRequest Attached Functions
event.initSession( Function callback ): Promise
- Initializes the session. This should be called in the beginning when you want to start the user sesion
- This will initialize a new session if one does not exist and fetch the old one if one exists
- The callback will return false if there was no error
####Attached Functionality:
event.session: Session
- This is the main class that should be used to manipulate the user session.
- There is no need to save the changes done to the session, that will be done automatically at the end of the request
####The Session exports the following functions:
hasSession(): Promise: Boolean
- Returns true if the user has a session started.
- Generally will be false before you call initSession
removeSession(): Promise: void
- Deletes the current session from the caching server directly
- Deletes the cookie as well
newSession(): Promise: String||Boolean
- Resolves to the new sessionId or to false if failed
add( String name, mixed value ): void
- Adds a new value to the session given a key
get( String key ): mixed
- Gets a value from the session
delete( String key ): void
- Deletes a key from the session
has( String key ): Boolean
- Checks if the session has the given key
saveSession( String sessionId = currentSessionId ): Promise: Boolean
- Save the current session
- The session id parameter is there for when switching sessions or creating new ones to not save the sessionId if it was not successfully created ( done internally )
- You probably should never pass a sessionId
####Exported Plugin Functions:
NONE
####Example:
- You can use the session like this:
const { Loggur } = require( 'event_request' );
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
// Initialize the session
app.add( async ( event )=>{
event.initSession( event.next ).catch( event.next );
});
// Redirect to login if authenticated is not true
app.add(( event )=>{
if (
event.path !== '/login'
&& ( ! event.session.has( 'authenticated' ) || event.session.get( 'authenticated' ) === false )
) {
event.redirect( '/login' );
return;
}
event.next();
});
app.post( '/login', async ( event )=>{
const result = event.validation.validate( event.body, { username : 'filled||string', password : 'filled||string' } );
if ( result.hasValidationFailed() )
{
event.render( '/login' );
return;
}
const { username, password } = result.getValidationResult();
if ( username === 'username' && password === 'password' )
{
event.session.add( 'username', username );
event.session.add( 'authenticated', true );
event.redirect( '/' );
}
else
{
event.render( '/login' );
}
});
app.listen( 80, ()=>{
Loggur.log( 'Server started' );
});#er_templating_engine
- Adds a templating engine to the event request ( the default templating engine is used just to render static HTML )
- If you want to add a templating engine you have to set the engine parameters in the options as well as a templating directory
####Dependencies:
NONE
####Accepted Options:
engine: Object
- Instance of a templating engine that has a function render
- The render function should accept html as first argument and object of variables as second
- Defaults to DefaultTemplatingEngine which can be used to serve static HTML
templateDir: String
- Where to draw the templates from
- Defaults to PROJECT_ROOT/public
####Events:
render ( String templateName, Object variables )
- Emitted in the beginning of the rendering process if everything has been started successfully
####EventRequest Attached Functions
event.render( String templateName, Object variables = {}, Function errorCallback = null ): Promise
- templateName will be the name of the file without the '.html' extension starting from the tempateDir given as a base ( folders are ok )
- The variables should be an object that will be given to the templating engine
- The promise will be resolved in case of a successful render. Note: you don't have to take any further actions, at this point the html has already been streamed
- The promise will be rejected in case of an error with the error that happened. Note: In case of an error no further actions are needed as event.next is going to be automatically called and the errorHandler will take care of the error
- If you want to handle the error yourself, an errorCallback must be provided
- 'render' event will be emitted by the EventRequest in the beginning with details on what is being rendered
####Attached Functionality:
event.templatingEngine: TemplatingEngine
- The templating engine to be used with the render function
- Defaults to DefaultTemplatingEngine
event.templateDir: String
- The absolute path to where the templates are held
- Defaults to path.join( PROJECT_ROOT, './public' )
####Exported Plugin Functions:
NONE
####Example:
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
app.apply( app.er_templating_engine, { templateDir: path.join( __dirname, './public' ) } );
// OR
app.apply( 'er_templating_engine' );
// OR
app.apply( app.er_templating_engine );
// OR
const PluginManager = server.getPluginManager();
const templatingEnginePlugin = PluginManager.getPlugin( app.er_templating_engine );
templatingEnginePlugin.setOptions( { templateDir : path.join( __dirname, './public' ), engine : someEngineConstructor } );
app.apply( templatingEnginePlugin );
// THEN
router.get( '/preview', ( event ) => {
// If you have a templating engine that supports parameters:
event.render( 'preview', { type: 'test', src: '/data' }, event.next );
// Otherwise the default one can only render html
event.render( 'preview', {}, event.next );
}
);#er_file_stream
- Adds a file streaming plugin to the site allowing different MIME types to be streamed
- Currently supported are :
- Images: '.apng', '.bmp', '.gif', '.ico', '.cur', '.jpeg', '.jpg', '.jfif', '.pjpeg', '.pjp', '.png', '.svg', '.tif', '.tiff', '.webp'
- Videos: '.mp4', '.webm'
- Text: '.txt', '.js', '.php', '.html', '.json', '.cpp', '.h', '.md', '.bat', '.log', '.yml', '.ini', '.ts', '.ejs', '.twig', '', '.rtf', '.apt', '.fodt', '.rft', '.apkg', '.fpt', '.lst', '.doc', '.docx', '.man', '.plain', '.text', '.odm', '.readme', '.cmd', '.ps1'
- Audio: '.mp3', '.flac', '.wav', '.aiff', '.aac'
- The VideoFileStream can be paired up with an HTML5 video player to stream videos to it
- The AudioFileStream can also be paired up with an HTML5 video player to stream audio to it
- An 'stream_start' event will be emitted by the EventRequest the moment the stream is going to be started
- Each file stream has a getType method that returns whether it is a video, text, image or audio
- Files with no extension will be treated as text files
####Dependencies:
NONE
####Accepted Options:
NONE
####Events:
stream_start ( FileStream stream )
- Emitted when the stream is successfully started
####EventRequest Attached Functions
event.streamFile( String file, Object options = {}, errCallback ): void
- This function accepts the absolute file name ( file ) and any options that should be given to the file stream ( options )
- This function may accept an errCallback that will be called if there are no fileStreams that can handle the given file, otherwise call it will call event.next() with an error and a status code of 400
event.getFileStream( file, options = {} ): FileStream | null
- This function accepts the absolute file name ( file ) and any options that should be given to the file stream ( options )
- This function will return null if no file streams were found or in case of another error
####Attached Functionality:
event.fileStreamHandler: Object
- Object containing one function: getFileStreamerForType( String file ): FileStream
####Exported Plugin Functions:
NONE
####Example:
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
const PluginManager = app.getPluginManager();
const fileStreamPlugin = PluginManager.getPlugin( 'er_file_stream' );
app.apply( fileStreamPlugin );
// OR
app.apply( app.er_file_stream );
// OR
app.apply( 'er_file_stream' );- Example of streaming data:
const fs = require( 'fs' );
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
app.get( '/data', ( event ) =>{
const result = event.validation.validate( event.query, { file: 'filled||string||min:1' } );
const file = ! result.hasValidationFailed() ? result.getValidationResult().file : false;
if ( ! file || ! fs.existsSync( file ) )
{
event.next( 'File does not exist' );
}
else
{
// You can use this if you want to maybe pipe the file stream to a transformation stream or in general
// do something else than piping it to the event.response
event.getFileStream( file ).pipe( event.response );
}
}
);
app.get( '/dataTwo', ( event ) =>{
const result = event.validation.validate( event.query, { file: 'filled||string||min:1' } );
const file = ! result.hasValidationFailed() ? result.getValidationResult().file : false;
if ( ! file || ! fs.existsSync( file ) )
{
event.next( 'File does not exist' );
}
else
{
event.streamFile( file );
}
}
);#er_logger
- Adds a logger to the eventRequest
- Attaches a dumpStack() function as well as log( data, level ) function to the process for easier access
- This can be controlled and turned off. The process.log( data, level ) calls the given logger
####Dependencies:
NONE
####Accepted Options:
logger: Logger
- Instance of Logger, if incorrect object provided, defaults to the default logger from the Loggur
attachToProcess: Boolean
- Boolean whether the plugin should attach dumpStack and log to the process
####Events:
NONE
####EventRequest Attached Functions
NONE
####Attached Functionality:
process.dumpStack(): Promise
- Logs the current stack
process.log( data, level ): Promise
- You can use the attached logger anywhere
####Exported Plugin Functions:
NONE
####Example:
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
const PluginManager = app.getPluginManager();
const loggerPlugin = PluginManager.getPlugin( 'er_logger' );
app.apply( loggerPlugin );
//OR
app.apply( 'er_logger' );
//OR
app.apply( app.er_logger, { logger: SomeCustomLogger, attachToProcess: false } );#er_body_parser_json, er_body_parser_form, er_body_parser_multipart, er_body_parser_raw
- Adds a JsonBodyParser, FormBodyParser or MultipartBodyParser bodyParsers respectively that can be set up
- They all implement the design principle behind the BodyParser
- These plugins are basically one and the same and even tho many may be added they will use a single body parser handler.
- There will not be multiple middleware that will be attached
- Parsers are fired according to the content-type header
- json parser supports: application/json
- form body parser supports: application/x-www-form-urlencoded
- multipart body parser supports: multipart/form-data
- er_body_parser_raw is a fallback body parser that will return the data as a raw string if no other parser supports the request. The default body parser has a limit of 10MB. It can optionally be added as a final parser manually to have it's maxPayloadLength changed
####Dependencies:
NONE
####Accepted Options:
#####MultipartFormParser:
maxPayload: Number
- Maximum payload in bytes to parse if set to 0 means infinite
- Defaults to 0
tempDir: String
- The directory where to keep the uploaded files before moving
- Defaults to the tmp dir of the os
#####JsonBodyParser: maxPayloadLength: Number
- The max size of the body to be parsed
- Defaults to 104857600/ 100MB
strict: Boolean
- Whether the received payload must match the content-length
- Defaults to false
#####RawBodyParser: maxPayloadLength: Number
- The max size of the body to be parsed
- Defaults to 10485760/ 10MB
#####FormBodyParser: maxPayloadLength: Number
- The max size of the body to be parsed
- Defaults to 10485760
strict: Boolean
- Whether the received payload must match the content-length
- Defaults to false
cleanUpItemsTimeoutMS: Number
- The time in milliseconds after which files will be attempted to be deleted on eventRequest finish
- Defaults to 100
####Events:
NONE
####EventRequest Attached Functions
NONE
####Attached Functionality:
event.body: Object
- Will hold different data according to which parser was fired
- Json and Form Body parsers will have a JS object set as the body
- The multipart body parser may have $files key set as well as whatever data was sent in a JS object format
event.rawBody: Object
- Will hold the RAW request received
- Json and Form Body parsers will have a JS object
- The multipart body parser will also have the rawBody set but will always be {}
####Exported Plugin Functions:
NONE
####Example:
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
// Add Body Parsers
app.apply( app.er_body_parser_json );
app.apply( app.er_body_parser_form );
app.apply( app.er_body_parser_multipart );
app.apply( app.er_body_parser_raw );
// Add body parsers with custom options
app.apply( app.er_body_parser_json, { maxPayloadLength: 104857600, strict: false } );
app.apply( app.er_body_parser_form, { maxPayloadLength: 10485760, strict: false } );
app.apply( app.er_body_parser_multipart, { cleanUpItemsTimeoutMS: 100, maxPayload: 0, tempDir: path.join( PROJECT_ROOT, '/Uploads' ) } );
app.apply( app.er_body_parser_raw, { maxPayloadLength: 10485760 } );
app.post( '/submit', ( event )=>{
console.log( event.body );
console.log( event.rawBody );
event.send();
});#er_response_cache Adds a response caching mechanism.
####Dependencies:
er_data_server
####Accepted Options:
NONE
####Events:
NONE
####EventRequest Attached Functions
NONE
####Attached Functionality:
Middleware: cache.request
- Can be added to any request as a global middleware and that request will be cached if possible
event.cacheCurrentRequest(): Promise
- Caches the current request.
- Will not cache the response if the response was not a String
####Exported Plugin Functions:
NONE
####Example:
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
const PluginManager = app.getPluginManager();
const cacheServer = PluginManager.getPlugin( app.er_data_server );
app.apply( cacheServer );
app.apply( PluginManager.getPlugin( app.er_response_cache ) );
// call event.cacheCurrentRequest() where you want to cache.
app.add({
route : '/',
method : 'GET',
handler : ( event )=>{
event.cacheCurrentRequest();
}
});
// OR You can create your own middleware that will be added to all requests
// you want to cache, no need to do it separately
app.add( async ( event )=>{
const pathsToCache = ['/', '/sth', 'test'];
if ( pathsToCache.indexOf( event.path ) !== -1 )
{
await event.cacheCurrentRequest().catch( event.next );
}
// Or use the router to match RegExp
});
// When setting a request to be cached, ttl and useIp may be passed that will overwrite the default options
app.add( async ( event )=>{
//**useIp** -> whether the user Ip should be included when caching. This allows PER USER cache. -> Defaults to false
//**ttl** -> time to live for the record. Defaults to 60 * 5000 ms
await event.cacheCurrentRequest( { ttl: 20 * 1000, useIp: true } ).catch( event.next );
});
// You can add it via a middleware to a specific route
app.get( '/', 'cache.request', ( event )=>
{
event.send( 'Hello World!' );
}
);#er_env
- Adds environment variables from a .env file to the process.env Object. In case the .env file changes
- This plugin will automatically update the process.env and will delete the old environment variables.
####Dependencies:
NONE
####Accepted Options:
fileLocation: String
- The absolute path to the .env file you want to use
- Defaults to PROJECT_ROOT
####Events:
NONE
####EventRequest Attached Functions
NONE
####Attached Functionality:
NONE
####Exported Plugin Functions:
NONE
####Example:
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
app.apply( 'er_env' );
app.add(( event )=>{
console.log( process.env );
event.send( 'Done' );
});
app.listen( 80 );#er_validation
- Does not attach any functionality
- Provides a Dynamic Middleware that can validate any EventRequest properties
####Dependencies:
NONE
####Accepted Options:
failureCallback: Function
- The plugin can be attached or setup to have a default failureCallback which will be taken if one is not provided
####Events:
NONE
####EventRequest Attached Functions
NONE
####Attached Functionality:
NONE
####Exported Plugin Functions:
validate( Object validationRules, Function failureCallback): Function
- This function generates a Dynamic Middleware
- This can validate any parameter of the EventRequest like: body/query/headers/etc
- It can validate multiple parameters at one time
- The validationRules must be an object with parametersToValidate pointing to validation skeletons
- If no failureCallback is provided then a generic error will be sent with the validation error directly
- if one is provided it must accept 3 parameters: ( EventRequest eventRequest, String validationParameter, ValidationResult validatrionResult )
- If the parameter you are trying to validate does not exist in the EventRequest object, then an Error is thrown
- After validation the validated params will be set in the EventRequest parameter that was validated: if you validated query for example the validated parameters will be set in the query object ( this way if any conversion was done by asserting a key is numeric or string for example, the correct type will be kept )
- You don't have to validate all the keys, the objects will be merged
- This plugin uses the built in validation suite
####Example:
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
app.apply( app.er_body_parser_multipart );
// This will validate the query parameters and the body
app.post( '/',
app.er_validation.validate( { query : { testKey: 'numeric||min:1||max:255' }, body: { test: 'numeric||range:1-255'} } ),
( event )=>{
event.send( { query: event.query, body: event.body } );
}
);
app.listen( 80, ()=>{ console.log( 'Server started on port 80' ); } );- Passing a custom failure callback
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
app.apply( app.er_body_parser_multipart );
// This will validate the query parameters and the body and will call the error callback
app.post( '/',
app.er_validation.validate(
{ query : { testKey: 'numeric||min:1||max:255' }, body: { test: 'numeric||range:1-255' } },
( event, validationParameter, validationResult )=>{
console.log( validationParameter );
console.log( validationResult );
event.send( 'ok' );
}
),
( event )=>{
event.send( { query: event.query, body: event.body } );
}
);
app.listen( 80, ()=>{ console.log( 'Server started on port 80' ); } );- When passing a default one and a custom one, the custom one will be used
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
// You have to apply the validation plugin
app.apply(
app.er_validation,
{
failureCallback: ( event, validationParameter, validationResult )=>{
console.log( validationParameter );
console.log( validationResult );
event.send( 'ok' );
}
}
);
// Alternatively you can do:
app.er_validation.setOptions({
failureCallback: ( event, validationParameter, validationResult )=>{
console.log( validationParameter );
console.log( validationResult );
event.send( 'ok' );
}
});
app.apply( app.er_body_parser_multipart );
// This will validate the query parameters and the body and will call the error callback
app.post( '/',
app.er_validation.validate(
{ query : { testKey: 'numeric||min:1||max:255' }, body: { test: 'numeric||range:1-255' } },
( event, validationParameter, validationResult )=>{
console.log( validationParameter );
console.log( validationResult );
event.send( 'ok' );
}
),
( event )=>{
event.send( { query: event.query, body: event.body } );
}
);
app.listen( 80, ()=>{ console.log( 'Server started on port 80' ); } );- When passing a default one if no failure callback is provided then the default one will be used
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
app.apply(
app.er_validation,
{
failureCallback: ( event, validationParameter, validationResult )=>{
console.log( validationParameter );
console.log( validationResult );
event.send( 'ok' );
}
}
);
app.apply( app.er_body_parser_multipart );
// This will validate the query parameters and the body and will call the error callback
app.post( '/',
app.er_validation.validate(
{ query : { testKey: 'numeric||min:1||max:255' }, body: { test: 'numeric||range:1-255' } }
),
( event )=>{
event.send( { query: event.query, body: event.body } );
}
);
app.listen( 80, ()=>{ console.log( 'Server started on port 80' ); } );#er_cors
- Adds commonly used CORS headers
- In case of an options request returns 204 status code
- Defaults to:
const defaults = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'POST, PUT, GET, DELETE, HEAD, PATCH, COPY',
};####Dependencies:
NONE
####Accepted Options:
origin: String
- The allowed origins
- Sets Access-Control-Allow-Origin
- Defaults to '*'
headers: Array
- he Access-Control-Allow-Headers response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request.
- Sets Access-Control-Allow-Headers
- Defaults to '*'
exposedHeader: Array
- Response header indicates which headers can be exposed as part of the response by listing their names.
- Sets Access-Control-Expose-Headers
- Defaults to '*'
methods: Array
- The Allowed methods
- Only returned in case of an OPTIONS request
- Sets Access-Control-Allow-Methods
- Defaults to 'POST, PUT, GET, DELETE, HEAD, PATCH, COPY'
status: Number
- The status code that will be returned in case of an options request
- Only returned in case of an OPTIONS request
- Defaults to 204
credentials: Boolean
- response header tells browsers whether to expose the response to frontend JavaScript code when the request's credentials mode (Request.credentials) is include.
- Sets Access-Control-Allow-Credentials
- Omitted by default
maxAge: Number
- indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.
- Sets Access-Control-Max-Age
- Maximum number of seconds the results can be cached.
- Firefox caps this at 24 hours (86400 seconds).
- Chromium (prior to v76) caps at 10 minutes (600 seconds).
- Chromium (starting in v76) caps at 2 hours (7200 seconds).
- Chromium also specifies a default value of 5 seconds.
- A value of -1 will disable caching, requiring a preflight OPTIONS check for all calls.
- Omitted by default
####Events:
NONE
####EventRequest Attached Functions
NONE
####Attached Functionality:
NONE
####Exported Plugin Functions:
NONE
####Example:
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
app.apply( app.er_cors, {
origin: 'http://example.com',
methods: ['GET', 'POST'],
headers: ['Accepts', 'X-Requested-With'],
exposedHeaders: ['Accepts'],
status: 200,
maxAge: 200,
credentials: true,
});
app.add(( event )=>{
event.send( event.response.getHeaders() );
});
app.listen( 80 );er_rate_limits
- Adds a Rate limits plugin to the server.
- The rate limits plugin can monitor incoming requests and stop/delay/allow them if they are too many
- The rate limits plugin will create a new rate_limits.json file in the root project folder IF one does not exist and useFile is set to true
- If one exists, then the existing one's configuration will be taken.
- Instead of passing fileLocation you can pass the rules directly as an array
- If you provide the same dataStore to two servers they should work without an issue
- If you don't provide a dataStore, then the er_data_server data store will be used. If that plugin is not set, then the default bucket one will be used
####Dependencies:
NONE
####Accepted Options:
fileLocation
- The absolute path to the rate limits json file.
- The rules will be validated.
- Defaults to ROOT DIR / rate_limits.json
dataStore
- The dataStore to use for the buckets
- Defaults to the LeakyBucket default DataStore
rules
- Optional parameter that if passed will have more weight than the rate_limits.json file.
- If this is passed the file will never be created/loaded
- The structure of this option must be the same as the one in the rate_limits.json file.
- The rules will be validated.
- Defaults to empty
useFile
- Optional parameter that determines if the rate limits should be fetched from a file ( specified by fileLocation ) or not
- Defaults to false
####Events:
rateLimited( String policy, Object rule )
- The policy will be which policy applied the rate limiting
- There may be more than one rateLimited event emitted
- Returns all the rule settings that rate limited the request
- This is emitted before any actions are taken
####EventRequest Attached Functions
NONE
####Attached Functionality:
event.rateLimited: Boolean
- Flag depicting whether the request was rate limited or not
eventRequest.erRateLimitRules: Array
- Will hold all the rules that the plugin has along with the buckets
####Exported Plugin Functions:
rateLimit( Object rule ): Function
- This function generates a Dynamic Middleware
- The rule provided will apply ONLY for this route.
- It will ALWAYS match
- You don't need to provide path or methods for this rule, they will be determined dynamically
- If you want multiple rate limiting rules to be applied then you can call this function as many times as you would like and pass the array of functions
- You don't have to apply the plugin in order to use the rateLimit function but if you want to provide a custom data store for a distributed environment then you have to.
- !!!WARNING!!!: Due to the way that middlewares work, this will be fired very very late. If you want to limit things like file transfers or authorization ( operations that cost resources ), then this approach may not be the best. Alternatively you can add a new middleware with the same route/method as the one you want to rate limit just before these costly operations and rate limit that.
####Notes: If you want to create custom rate limiting you can get er_rate_limits plugin and use getNewBucketFromOptions to get a new bucket, given options for it options['maxAmount'] options['refillTime'] options['refillAmount']
Rate limit can be applied to different routes and different HTTP methods Rate limit rule options:
path: String
- the url path to rate limit ( blank for ALL )
methods: Array
- the methods to rate limit ( blank for ALL )
maxAmount: Number
- The maximum amount of tokens to hold
refillTime: Number
- the time taken to refill the refillAmount of tokens
refillAmount: Number
- the amount of tokens to refill when refilling happens
policy: String
- The type of rate limiting to be applied
delayTime: Number
- must be given if policy is connection_delay. After what time in seconds should we retry
delayRetries: Number
- must be given if policy is connection_delay. How many retries to attempt
stopPropagation: Boolean
- Whether to stop if the rate limiting rule matches and ignore other rules
- Defaults to false
- Optional
ipLimit: Boolean
- whether the rate limiting should be done per ip
- Defaults to false
- Optional
####POLICIES:
PERMISSIVE_POLICY = 'permissive';
This policy will let the client connect freely but a flag will be set that it was rate limited
CONNECTION_DELAY_POLICY = 'connection_delay';
This policy will rate limit normally the request and will hold the connection until a token is freed If this is the policy specified then delayTime and delayRetries must be given. This will be the time after a check should be made if there is a free token. The first connection delay policy hit in the case of many will be used to determine the delay time but all buckets affected by such a connection delay will be affected
STRICT_POLICY = 'strict';
This policy will instantly reject if there are not enough tokens and return an empty response with a 429 header. This will also include a Retry-After header. If this policy is triggered, stopPropagation will be ignored and the request will be immediately canceled
####Example:
[
{
"path": "",
"methods": [],
"maxAmount": 10000,
"refillTime": 10,
"refillAmount": 1000,
"policy": "connection_delay",
"delayTime": 3,
"delayRetries": 5,
"stopPropagation": false,
"ipLimit": false
}
]/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
const { Server } = require( 'event_request' );
const RateLimitsPlugin = require( 'event_request/server/plugins/available_plugins/rate_limits_plugin' );
const DataServer = require( 'event_request/server/components/caching/data_server' );
app.apply( app.er_rate_limits );
// If you implement a custom distributed DataServer you can sync between servers
// Two servers
const dataStore = new DataServer( { persist: false, ttl: 90000 } );
const appOne = new Server();
const appTwo = new Server();
appOne.apply( new RateLimitsPlugin( 'rate_limits' ), { dataStore } );
appTwo.apply( new RateLimitsPlugin( 'rate_limits' ), { dataStore } );- Using the global middleware
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
const rule = {
"maxAmount":1,
"refillTime":100,
"refillAmount":1,
"policy": 'strict',
"delayTime": 3,
"delayRetries": 5,
"stopPropagation": false,
"ipLimit": false
};
// No need to apply this
app.apply( app.er_rate_limits );
app.get( '/testRoute', app.er_rate_limits.rateLimit( rule ), ( event )=>{
event.send( name );
});
const ruleTwo = {
"maxAmount":1,
"refillTime":100,
"refillAmount":1,
"policy": 'permissive',
"delayTime": 3,
"delayRetries": 5,
"stopPropagation": false,
"ipLimit": false
};
app.get( '/testRouteTwo', [
app.er_rate_limits.rateLimit( ruleTwo ),
app.er_rate_limits.rateLimit( rule )
], ( event )=>{
event.send( name );
});- Adding with rules directly:
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
const { Server } = require( 'event_request' );
const RateLimitsPlugin = require( 'event_request/server/plugins/available_plugins/rate_limits_plugin' );
const DataServer = require( 'event_request/server/components/caching/data_server' );
const rules = [
{
"path": "",
"methods": [],
"maxAmount": 10000,
"refillTime": 10,
"refillAmount": 1000,
"policy": "connection_delay",
"delayTime": 3,
"delayRetries": 5,
"stopPropagation": false,
"ipLimit": false
},
{
"path": "/test",
"methods": [],
"maxAmount": 10000,
"refillTime": 10,
"refillAmount": 1000,
"policy": "connection_delay",
"delayTime": 3,
"delayRetries": 5,
"stopPropagation": false,
"ipLimit": false
},
];
app.apply( app.er_rate_limits, { rules } );
// If you implement a custom distributed DataServer you can sync between servers
// Two servers
const dataStore = new DataServer( { persist: false, ttl: 90000 } );
const appOne = new Server();
const appTwo = new Server();
appOne.apply( new RateLimitsPlugin( 'rate_limits' ), { dataStore } );
appTwo.apply( new RateLimitsPlugin( 'rate_limits' ), { dataStore } );#er_security
- Adds common security http headers
- Options for all the headers can be passed directly in the options and later changed as all components used by the security plugin implement a builder pattern
####Dependencies:
NONE
####Accepted Options:
build: Boolean
- Whether the headers should be build and set immediately ( taking the default settings )
- Defaults to true
csp: Object
- The Content Security Policy options
- This object will be passed to the CSP component
- For the object supported parameters, look further down
- Defaults to an empty object
hsts: Object
- The HTTP Strict Transport Security options
- This object will be passed to the HSTS component
- For the object supported parameters, look further down
- Defaults to an empty object
ect: Object
- The Expects CT options
- This object will be passed to the Expects-CT component
- For the object supported parameters, look further down
- Defaults to an empty object
cto: Object
- The Content Type Options options
- This object will be passed to the Content Type Options component
- For the object supported parameters, look further down
- Defaults to an empty object
####Events:
NONE
####EventRequest Attached Functions
event.$security.build(): void
- This function accepts no arguments.
- It is used to set all the security headers
- This function is called if the build flag is set
####Attached Functionality:
event.$security: Object
- Holds the build function that builds and sets the security headers
- Holds all the security modules
- These modules can be accessed and used anywhere
event.$security.csp: ContentSecurityPolicy
- Class that implements a builder design pattern
- Look down for more info
event.$security.cto: ContentTypeOptions
- Class that implements a builder design pattern
- Look down for more info
event.$security.hsts: HttpStrictTransportSecurity
- Class that implements a builder design pattern
- Look down for more info
event.$security.ect: ExpectCT
- Class that implements a builder design pattern
- Look down for more info
####Exported Plugin Functions:
NONE
####Objects:
#####HTTP Strict Transport Security
- Used to build a Strict-Transport-Security header
- It can either be enabled or not
- The HTTP Strict-Transport-Security response header (often abbreviated as HSTS) lets a web site tell browsers that it should only be accessed using HTTPS, instead of using HTTP.
- More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
- Accepted options:
enabled: Boolean
- Whether the plugin should be enabled or not
- Defaults to true
maxAge: Number
- The time, in seconds, that the browser should remember that a site is only to be accessed using HTTPS
- Defaults to 31536000
includeSubDomains: Boolean
- Optional
- If this optional parameter is specified, this rule applies to all of the site's subdomains as well.
- Defaults to false
preload: Boolean
- Optional
- See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security#Preloading_Strict_Transport_Security
- Defaults to false
- Functions:
getHeader(): String
- Returns the header as it should be set.
build(): String
- Builds the header string from all the directives called before hand
setMaxAge( Number maxAge ): void
- Sets the max age
- The value is in seconds
- If it is invalid, default will be left
setEnabled( Boolean enabled = true ): void
- Enables the plugin
- You can pass false and the plugin will be disabled
preload( Boolean preload = true ): void
- Sets preload state
- If it is invalid, default will be left
includeSubDomains( Boolean include = true ): void
- Sets includeSubDomains state
- If it is invalid, default will be left
#####Content Type Options
- Used to build a X-Content-Type-Options header
- It can either be enabled or not
- The value of the header is nosniff always
- The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed. This allows to opt-out of MIME type sniffing, or, in other words, it is a way to say that the webmasters knew what they were doing.
- More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
- Accepted options:
enabled: Boolean
- Whether the plugin should be enabled or not
- Defaults to true
- Functions:
getHeader(): String
- Returns the header as it should be set.
build(): String
- Builds the header string from all the directives called before hand
#####Expect-CT
- Used to build a Expect-CT header
- It can either be enabled or not
- The Expect-CT header allows sites to opt in to reporting and/or enforcement of Certificate Transparency requirements, which prevents the use of misissued certificates for that site from going unnoticed.
- More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT
- Accepted options:
enabled: Boolean
- Whether the plugin should be enabled or not
- Defaults to true
maxAge: Number
- Specifies the number of seconds after reception of the Expect-CT header field during which the user agent should regard the host from whom the message was received as a known Expect-CT host.
- Defaults to 86400
enforce: Boolean
- Optional
- Signals to the user agent that compliance with the Certificate Transparency policy should be enforced (rather than only reporting compliance) and that the user agent should refuse future connections that violate its Certificate Transparency policy.
- Defaults to true
reportUri: String
- Optional
- Specifies the URI to which the user agent should report Expect-CT failures.
- Defaults to ''
- Functions:
setEnabled( Boolean enabled = true ): void
- Enables the plugin
- You can pass false and the plugin will be disabled
enforce( Boolean enforce = true ): void
- Sets the enforcement state
- If it is invalid, default will be left
setReportUri( String reportUri ): void
- Sets the report uri
- If it is invalid, default will be left
setMaxAge( Number maxAge ): void
- Sets the max age
- The value is in seconds
- If it is invalid, default will be left
getHeader(): String
- Returns the header as it should be set.
build(): String
- Builds the header string from all the directives called before hand
#####Content Security Policy
- Used to build a CSP header
- Many the directives may have many arguments, when the header is build only one directive will be set.
- Accepted options:
enabled: Boolean
- Whether the plugin should be enabled or not
- Defaults to true
directives: Object
- Holds all the directives for the that should be added
- Supports all directives from: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
- The directives should be added exactly as they are specified in the documentation (script-src, style-src, frame-ancestores, etc)
- For directives that don't have a value like lets say 'sandbox' they should be passed with an empty array:
sandbox: [] - Single quotes will be added to the directives if needed, so it's safe to pass
self,unsafe-eval, etc without single quotes - Defaults to an empty object
xss: Boolean
- This flag will enable some directives used to battle XSS attacks
- This adds src for every directive
- This adds upgradeInsecureRequests directive as well
- Defaults to true
self: Boolean
- This flag will add origin self to the default-src
- Defaults to false
sandbox: Boolean
- This flag will enabled sandbox mode
- Defaults to false
reportUri: String
- If this flag is given, then the plugin will be set to reporting only mode
- Defaults to null
useReportTo: Boolean
- This flag must be used with reportUri otherwise it won't work.
- If this flag is set then the new report-to will be used
- Defaults to false
- Functions:
xss(): void
- Enables xss protection
- Same as setting the xss flag in the options
allowPluginType( String mimeType ): void
- Adds a 'plugin-types' directive with the given mimeType
- The mimeType will be checked against /^[-\w.]+/[-\w.]+$/
setEnabled( Boolean enabled = true ): void
- Enables the plugin
- You can pass false and the plugin will be disabled
getHeader(): String
- Returns the header as it should be set.
- The header can be influenced if reporting is enabled
setReportOnly( String uri ): void
- If uri is not provided then this will do nothing
- Sets the state to report only, which changes the header to be CSP report only
- Adds a directive 'report-uri' with the given uri
setReportOnlyWithReportTo( String uri ): void
- If uri is not provided then this will do nothing
- Sets the state to report only, which changes the header to be CSP report only
- Adds a directive 'report-to' with the given uri
- report-to is not supported by some browsers, so you should probably call setReportOnly with a uri as well
enableSandbox(): void
- Enables sandbox mode: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
- Adds a directive 'sandbox' to the csp
allowSandboxValue( String value ): void
- Enables sandbox mode: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
- Adds a directive 'sandbox' to the csp with a given value
upgradeInsecureRequests(): void
- Adds a directive 'upgrade-insecure-requests'
- This will upgrade all http links in your site to https automatically
addBaseUri( String uri ): void
- Adds a 'base-uri' directive with the given uri
- This will add single quotes if needed
addFrameAncestors( String uri ): void
- Adds a 'frame-ancestors' directive with the given uri
- This will add single quotes if needed
addScriptSrc( String uri ): void
- Adds a 'script-src' directive with the given uri
- This will add single quotes if needed
addImgSrc( String uri ): void
- Adds a 'img-src' directive with the given uri
- This will add single quotes if needed
addChildSrc( String uri ): void
- Adds a 'child-src' directive with the given uri
- This will add single quotes if needed
addConnectSrc( String uri ): void
- Adds a 'connect-src' directive with the given uri
- This will add single quotes if needed
addConnectSrc( String uri ): void
- Adds a 'connect-src' directive with the given uri
- This will add single quotes if needed
addDefaultSrc( String uri ): void
- Adds a 'default-src' directive with the given uri
- This will act as a fallback to ANY src directive
- This will add single quotes if needed
enableSelf(): void
- Adds a 'default-src' directive with the 'self'
addFontSrc( String uri ): void
- Adds a 'font-src' directive with the given uri
- This will add single quotes if needed
addFrameSrc( String uri ): void
- Adds a 'frame-src' directive with the given uri
- This will add single quotes if needed
addFrameSrc( String uri ): void
- Adds a 'frame-src' directive with the given uri
- This will add single quotes if needed
addManifestSrc( String uri ): void
- Adds a 'manifest-src' directive with the given uri
- This will add single quotes if needed
addMediaSrc( String uri ): void
- Adds a 'media-src' directive with the given uri
- This will add single quotes if needed
addObjectSrc( String uri ): void
- Adds a 'object-src' directive with the given uri
- This will add single quotes if needed
addStyleSrc( String uri ): void
- Adds a 'style-src' directive with the given uri
- This will add single quotes if needed
build(): String
- Builds the header string from all the directives called before hand
- You can modify directives after calling build and then call build again to get a new result
####Example:
- Apply the plugin with defaults
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
// It's a good idea to do this first before attaching any other plugins or adding routes
app.apply( app.er_security );- Apply the plugin and use the builder methods
app.apply( app.er_security );
app.add(( event )=>{
event.$security.csp.enableSandbox();
event.$security.hsts.setEnabled( false );
event.$security.cto.setEnabled( false );
event.$security.ect.setMaxAge( 300 );
event.$security.ect.setReportUri( '/report/uri' );
event.$security.build();
event.next();
});- Apply the plugin with custom directives
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
app.apply( app.er_security, {
csp : {
directives : {
'font-src' : ['https://fonts.gstatic.com'],
'script-src': ['https://example.com'],
'style-src': ['https://example.com', 'unsafe-eval'],
},
xss: true
},
ect : {
maxAge: '300'
},
hsts : {
maxAge: '300',
preload: false
},
cto : {
enabled: false
},
build: true
});- Apply the plugin with a lot of different commands later, as well as rebuilding
/**
* @brief Instantiate the server
*/
const app = require( 'event_request' )();
app.apply( app.er_security, { csp : { xss: false } } );
app.add(( event )=>{
// self is repeated twice but will be shown only once and with single quotes
event.$security.csp.addFontSrc( 'self' );
event.$security.csp.addFontSrc( "'self'" );
event.$security.csp.addFontSrc( 'test' );
event.$security.csp.upgradeInsecureRequests();
event.$security.csp.enableSelf();
event.$security.csp.enableSandbox();
event.$security.ect.setEnabled( false );
event.$security.ect.setMaxAge( 30000 );
event.$security.hsts.setMaxAge( 300 );
// null and 'string' are invalid for max age so 300 will be left
event.$security.hsts.setMaxAge( null );
event.$security.hsts.setMaxAge( 'string' );
event.$security.hsts.preload();
event.$security.hsts.includeSubDomains( false );
event.$security.build();
// This will actually add a new script-src to the csp and will disable the cto component
event.$security.csp.addScriptSrc( 'test' );
event.$security.cto.setEnabled( false );
// This will overwrite the previous build and set the new modified headers
event.$security.build();
event.next();
});