Entirely node.js based web framework including REST API and socket.io support, script compression and SQL integration. AND: Newly added plugin support and SQL pooling.
To set up the server runtime,
const LxWebApplication = require("lux-web-system")
/**
* This is the DEFUALT options object, that
* can be overwritten partially or even completely
* by submitted options. Also works recursively.
*
* You DO NOT NEED to assign every field, but if you encounter
* errors it might help to try setting the .sql option.
*
* The SSL option is REQUIRED
*/
const options = {
name: "LUX Web Application",
configs: [
"Program"
],
directories: {
config: '/data/',
workspace: '/',
},
sql: false,
ssl: {
key: null,
cert: null
},
session: false,
session_expire_time: 3600,
session_domain: null,
server: {
port: 8080,
frontend: '/web',
endpoint: '/api',
middlewares: []
},
api_cors: false,
ddos: {
weight: 1,
maxWeight: 10,
checkInterval: 1000
},
template_fields: {}
}
const {Query, Conf, Log, Color, Use} = LxWebApplication(options);
The application initializer will create all directories needed and furthermore returns some useful functions to interact with the running server. These functions are:
Query
Promise-based SQL caller. This is using the managed SQL connection pool created within the application and returns a Promise resolving into the call result.
Usage:Query(query_string, fields)
Query.GetConnection()
Promise-based. Returns a dedicated, free connection from the SQL pool (Connection
).Connection()
Identical toQuery()
, but using the dedicated connection instead of a randomly free connection every time (likeQuery()
does)Connection.Release()
IMPORTANT: Release the connection after using it, so it can be reused.
Conf
Configuration holder. Contains all configuration elements named by the pattern defined in theconfigs
-section within the options object (Case Sensitive)
For example the "program.yml" configuration from the default options will be accessible atConf.Program
Log
Strongly recommended to use this instead ofconsole.log
.
Usage:Log(env_name, ...args)
Color
Console color holder. See reference for more.Use
Register a plugin instance on the system. For more details, refer to the plugins section below.
Usage:Use(plugin_constructor, ?permissions)
permissions
(optional): An object declaring the plugin's permissions. At the moment, there is only need for this if the plugin requires to use SQL querying, in which casepermissions
should look like this:{ sql: true }
. Otherwise, this field can be completely omitted.
Notice: Plugins will modify the automatically generated lux.js file. If you add or remove plugins, it may be required to clear the cache manually in your browser. An easy workaround for this is to append an increasing query to the src tag (
js:lux.js?2
etc.)
Please try not to overwrite these functions, because a second call of the LxWebApplication function is not possible.
IMPORTANT: There is no default and built-in DDoS protection / rate limiting in the system anymore. This way users can address security concerns on their own and the way they prefer best. It is though highly recommended to set up a protection mechanism such as rate-limiter-flexible or dddos yourself, which can be added as a middleware to
options.server.middlewares
.
The following options are available to modify the initializer of LxWebApplication
name
: string
The environment name. Will be shown in Logs and API not-found promptsconfigs
: Array<string>
All the configurations that should be read. More information about that in the config sectiondirectories
: Objectconfig
: string
Root-relative path of the folder the config files will be stored in. Has to end with /workspace
: string
Root-relative path of the folder the endpoint and frontend folders will be placed. Has to end with /
sql
: Object - Defaults tofalse
, meaning no SQL manager will be loaded andQuery
will not be working. Once an object is submitted, it will overwrite the defaultsconnectionLimit
:number . Defazkts to60
The maximum number of connections managed by the connection poolhost
: string - Defaults tolocalhost
SQL server hostuser
: string - Defaults toroot
SQL usernamepassword
: string - Defaults toroot
SQL user passwordcharset
: string - Defaults toutf8mb4
SQL charset to use.database
: string - Defaults to""
SQL database to select
ssl
: Object (required)key
: string
Path to the SSL private key filecert
: string
Path to the SSL certificate file
session
: boolean
Whether or not to use sessions in the API endpointsession_expire_time
: number
Time in seconds sessions will last until they expiresession_domain
: string
If set, stores the session cookie for this domainserver
: Objectport
: number
Port to bind the application tofrontend
: string
URL where the frontend will be found. Not ending with /endpoint
: string
URL where the endpoint and the socket will be found. Not ending with /middlewares
: Function[]
An array of express.js middleware functions to use on a global scope. It is recommended to use a rate limiter at this point, to prevent too many requests or DDoS attacks.
api_cors
: string
Defaults tofalse
meaning that no CORS policy will be loaded
If set to a domain string, API will use this domain string as CORS policy domaintemplate_fields
: Object | string
Assign the fields for the template replacement to assign. This replacement is primarily dedicated to CSS and JS resource files.
If is a string, it will specify a config file and path to look after. This shall be provided in the formatconfname:path.to.holder
(withconfname
being the name of the config file). If there is no sub object that holds the field information, the path can be left empty:confname:
The fronend working environment can be found inside the folder frontend
, generated inside of directories.workspace
. Its structure must not be changed and looks like this:
frontend
└───css
└───html
└───img
└───js
Depending on the server.frontend
setting, all of the frontends content will be found at this very path.
... is comparable to the www/html directory of Apache servers. It MUST contain an index.html file, along with all other regular files that should be used with the frontend. When a specific file at a given URL cannot be found, the app will automatically fallback to the index.html. Otherwise, the folder works similar to a normal web server structure, meaning any file can be found at its relative path URL.
These folders are meant to serve the resource files, where the files will automatically be wrapped up in a single compressed file. Resource files can be accessed at the URL <frontend>/css:main.css
or similar.
The system will wrap multiple source files of the same name group into one compressed source file. Example: app.js
, app.test.js
and app.system.js
will be found compressed together at <frontend>/js:app.js
.
The system will automatically replace any template strings inside CSS and JS files before compression. If you have set up some object fields on template_fields
in the LxWebApplication(...)
constructor options, they will be applied.
For instance, if template_fields
looks like so:
{
something: "bananas",
number: 23
}
The compressor will output something like this:
// Before replacement
const {{something}}_info = "I have got {{number}} {{something}}";
// After replacement
const bananas_info = "I have got 23 bananas";
Even though images can also be stored inside the HTML folder, the <frontend>/img:*
URL provides some kind of uniformness. Primarily intended to serve images only.
This library is located at <frontend>/js:lux.js
. It already contains the jQuery and socket.io libraries, along with a LUX Web System API.
NOTICE: Due to the reserved namespace of
lux.js
, files in the frontendsjs
-folder named after alux.*.js
pattern will not be served.
The LUX API can be constructed as follows:
const {Call, Socket, Page, Plugins} = Lux({
backend: "<PATH-TO-ENDPOINT>", // Optional, defaults to /api
socket: true // Whether or not a socket connecion
// should be created
})
Call(api, data, method)
Calls a REST API endpoint
Returns a Promise resolving to the call result.api
: The backend relative API endpoint. Ex:/api/test
would be called withCall('test')
data
(optional) The data object. If set, the method will automatically change toPOST
method
(optional) The HTTP method the request should be sent with. Defaults toGET
without data andPOST
with data.Call.use(middleware)
Registers a middleware that will be executed before the Call Promise resolves.middleware
represents a function with one argument object passed:xhr
The XHR request objectdata
The returned datacancel
By default set totrue
. If set tofalse
by the middleware, the request will be canceled and the Promise will resolve with status code -1
- Resolves to
{code, data}
code
The HTTP status / response codedata
The data returned from the endpoint
Socket
Object that holds the most important socket handling functons.Emit(event, ...args)
Emits a socket.io event. Returns a Promise resolving to the server response, if existing.
Unanswered events will never resolve the Promise!event
The event that should be sent to the server...args
All arguments that should be passed with the event
.On(event, callback)
Registers a global callback for when the server sends an event.Connection(callback)
Registers a callback for connection changes. Passedtrue
on connection,false
on disconnection
Page
Page(path)
Dynamic URL handling. Calling this function switches to the desired path without reloading the page. Path changes, as well as Browser Navigation Events, can be handled with:Page(callback)
This should always happen first. Within this function you can handle what happens if a specific URL has been called.Page.Args
Array that contains all arguments of the path (/arg0/arg1/arg2 ...
)
Will be updated before the page handle callback is invokedPage.Path
Similar to window.location.href
Plugins
Collection of all loaded plugin frontend libraries. Because plugins are loaded asynchronously, it is possible for the plugins to load a little bit later. To capture this,
On first run, an endpoints
folder will be created inside the directories.workspace
folder. This folder will by default contain two template files, that never should be renamed or deleted: api.js
and socket.js
.
Both contain examples of their basic usage. All changes to these files will be monitored by the system, and once any changes get detected, the API or the Socket will be refreshed automatically. You always can create subfiles with a similar name scheme (api.v1.js
or socket.test.js
etc.), which will be monitored too.
module.exports = (On, Log, Query, Conf, Util, Emit) => {
On('get', '/test', (req, res) => {
res.json({
result: "This is an example response",
second_value: 11880
})
})
...
}
On(method, path, ...handlers)
Will act exactly likeapp.<method>(path, ...handlers)
in express.jsmethod
The HTTP method that should be listened topath
express-pattern matched path... handler(req, res[, next])
express.js handler / middlewares
On.After(callback)
Will register a callback that will be executed after any actual API endpoint handlers are executed. You can see this as a late-in-time-middleware, that will be executed after all API requests are finished.
Please notice that any registered callbacks of this kind will always be executed on any API call.Log(...args)
Web framed loggerQuery
Identical withLxWebApplication.Query
.UID(table, ?length, ?fieldname, ?charset])
Generates a random ID on a table fields in the SQL databasetable
The name of the SQL table to addresslength
(optional) The ID length. Default 30fieldname
(optional) The table column name. Default 'id'charset
(optional) String of characters the UID will be generated from. Defaults toUtil.UID.CHSET_64
Conf
Identical withLxWebApplication.Conf
Util
Utility holder.time(?ms)
Returns current UNIX timestampms
(optional) A millisecond timestamp to convert from. Otherwise, will be Date.now()
.daystamp(?ts)
Returns the current daystampts
(optional) A unix timestamp. Otherwise, will be the current unix timestamp.
.Wait(ms)
Promise-based waiting function.Assign(source, ...assignables)
Multi-Level object assign helper function.UID(length, ?charset)
Generates a random IDlength
Character Lengthcharset
(optional) string containing the characters to use. Defaults toUtil.UID.CHSET_64
.UID
.CHSET_64
A base 64 charset for UIDs.CHSET_FULL
A full alphabet & numbers charset.CHSET_UPPER
A full uppercase alphabet & numbers charset.CHSET_UCLET
An uppercase letters charset
Emit(event, ...args)
socket.io event emitter
Works similar to api.js
, but with a slightly different handler function:
module.exports = (On, Log, Query, Conf, Util, Sock) => {
On('test', data => {
Log("Received data", data)
return {received: data, time: Util.time()};
})
}
On(event, handler)
Registers an event handler at the socket.io instanceevent
Name of the event the server should listen tohandler(data)
Handler function that returns the response sent back to the requesting client.
Sock
Contains the server's socket information.socket
The socket the command has been issued from.io
The global socket.ioio
object
- All other fields identical to api.js
Plugins can extend the functionality of the web system dramatically by being able to hook into both backend and frontend. They are an easy, flexible and reusable way of adding new or complicated behavior to the web application.
It's just as simple as that:
const plugin1 = require('./my-local-plugin'); // Loads a JS file
const plugin2 = require('some-other-plugin-module'); // Loads a npm module
const App = LxWebApplication(options);
App.Use(plugin1);
App.Use(plugin2, {sql: true});
Or if you prefer to use it inline:
const App = LxWebApplication(options);
App.Use(require('./my-local-plugin'));
Unfortunately you cannot stack these into one function because of the optional permissions
parameter. But hey - there are worse problems, don't you think?
For example the case in which you prefer to declare your plugins completely hardcore-inline - which you can, but definately shouldn't.
const App = LxWebApplication(options);
App.Use(($) => {
$.Begin('iamcrazy');
...
});
IMPORTANT: Please keep in mind, that any changes to plugin usage within the system will affect the automatically generated lux.js file, which is cache controlled by browsers. You may need to manually force a cache revalidation on this file once you apply modifications to your plugin setup.
If you decide to write your own plugins for LUX Web System, theres alomst nothing easier than that. Either you create a new, dedicated node.js module for being able to easily reuse and redistribute your libraries in the future, or you do it locally. Either way, the first step should be creating the main plugin file. Whether or not you work with further modules or not is completely up to you, but the main syntax and workflow of the plugin creation process shoult be maintained.
Please try to give your plugin an identifier name, that has a low chance of being used by other developers, due to the fact that names can only be used once inside a system instance.
The main plugin and/or module index file. This one should be imported with Use(...)
.
module.exports = ($) => {
// Before anything can happen, the plugin should tell the system how it is called.
// This step is inevitable and MUST happen first.
//
// (Please replace <plugin_name> with the unique name of your plugin)
$.Begin('<plugin_name>');
// We can now add some resource files to the plugin that the plugin
// uses at the frontend.
// Unfortunately, the full system path must be passed to this.
$.FrontendFile(__dirname + '/frontend.js');
$.FrontendFile(__dirname + '/fancy-design.css');
// Let's register an API endpoint for our plugin.
// In this instance, the endpoint can be found
// at /<api_path>/@/<plugin_name>/123
$.API('get', '/123', (req, res) => {
res.json({result: 'OK'});
})
// Now, let's create a websocket command handler.
// In this case, the command this handler responds to
// will be named @<plugin_name>:123
$.Socket('123', (data) => {
return {message: 'Got your data!', received: data};
})
// We can even register one ore more globally scoped express
// middlewares on the server if we desire.
$.Middleware((req, res, next) => {
$.Log('Plugin middleware was just called on path', req.path);
next();
})
}
There are some other useful helper functions and objects that you can use from inside the plugin. They work similar to their equally named counterparts in api.js and socket.js.
$.Query()
// Use the async SQL query function. FALSE if no permission
// Requires a permission scope to function and depends on whether
// or not the sql option has been set on initialization.
$.Config
// Access the programs configuration section.
$.Log()
// Outputs "Plugin (<name>): ..." log message
$.Emit()
// Emits a global web socket event
$.Util
// Utility helper
$.WorkspacePath
// The OPTIONS value for the workspace directory
In case you declared some frontend files, they will be automatically added to the main framework and, once loaded by the frontend, can be accessed as described in the FRONTEND section. The only real requirement is for the Lux()
constructor being called. The system will then try and load the respective frontend scripts, style sheets will as well be applied to the document automatically.
IMPORTANT: Please keep in mind, that any changes to plugin usage within the system will affect the automatically generated lux.js file, which is cache controlled by browsers. You may need to manually force a cache revalidation on this file once you apply modifications to your plugin setup.
The javascript that gets served as frontend plugin.
const plugin = {};
plugin.SomePublicFunction = () => {
alert('Hey everybody! This is the plugin speaking.');
}
export const Enable = ($) => {
plugin.CallMyBackend = async () => {
const result = await $.Call('123');
console.log(result);
}
return plugin;
}
Your frontend file needs the Enable
-function being exported, which is being called once at startup. The $
-object being passed to it contains the exact same fields as the Lux()
-Constructor returns. The only real difference is the behavior of Call
, which, by default, calls the plugin's internal APIs located at <api_path>/@/<plugin_name>/...
. To make a non-scoped, global API call, you can prepend a slash to the path. $.Call('/123')
Notice, that
$.Plugins
will most likely not contain every plugin on startup, due to the plugins being loaded asynchronously.
This system uses many external modules that are included as peer dependencies, and without which the system would not work that good. You can find a list of all dependencies in the dependencies tab. Many, many thanks to all the authors, developers and extensions making this project possible!
Released under the MIT license