AEX - Next Generation Web Framework for javascript backends, A Realisation of The Web Straight Line Theory
for node.js/bun.js apis, where The Web Straight Line Theory
is for All Modern Web Frameworks
- node.js
- bun.js
import { Aex, http } from "@aex/core";
class Helloworld {
public message: string;
constructor() {
this.message = "Hello world!";
}
@http("/")
public async all(req: any, res: any) {
res.end(this.message);
}
@compact("/")
public async compactAll(ctx: any) {
ctx.res.end(this.message);
}
@http("*", "*", true)
public async httpCompactAll(ctx: any) {
ctx.res.end(this.message);
}
}
const aex = new Aex();
aex.push(Helloworld);
// aex.prepare().start(8086).then();
await aex.prepare().start(8086);
A simple, easy to use, decorated, scoped, object-oriented web server, with async linear middlewares and no more callbacks in middlewares.
It can be used as a micro service server and a very large scalable enterprise web server with official or customized decorators plugged-in.
It is a web framework based on decorators for javascript backend(node.js, bun.js).
So you should enable decorators
to use it.
- JavaScript users please refer https://babeljs.io/docs/en/babel-plugin-proposal-decorators for detailed information. Or simply clone this starter to start a new aex project: https://github.com/aex-ts-node/aex-babel-node-js-starter
- TypeScript users please refer https://www.typescriptlang.org/docs/handbook/decorators.html for more information.
- Keep in mind to separate the web logic from the business logic and develope only for the web logic.
- Focus exclusively on web flow.
- Simplify the way to create good web projects.
- Think of web interactions as phrased straight lines, which we call the Web Straight Line.
- No MVC, but focus on the architecture that is the web logic.
Web Straight Line is used to describe the phrases of the processes on the http/web request.
It can be breifly described as the following diagram:
The Web Straight Line
Web Straight Line is a true web server theory for the web framework (as opposed to MVC thoery which is more appropriate for desktop applications) that solves only the problems caused by the web (namely the HTTP protocol) itself.
It can be trimmed as a micro service web server or a full-fledged web server by documenting enough constraints using docrators, it is logically scalable using decorators/middlewares.
- Install
- Quick Start
- Framework functions
- Decorators
- Usage with no decorators
- Middlewares
- Scope
- Helper
- Express Middleware Integration
- Get the web server
- Websocket support
- Upgrade from Expressjs
-
HTTP method decorators (
@http
,@get
,@post
,@compact
) -
Data parsing decorators (
@formdata
,@query
,@body
) -
Static file serving decorators (
@serve
) -
Template decorators (
@template
) -
Session management decorators (
@session
) -
Data filtering and validation decorators (
@filter
) -
Error definition decorators (
@error
) -
Custome middleware decorators (
@inject
) -
API development decorators (
@rest @api
) -
Event messaging decorators (
@listen
)
if you use npm
npm install @aex/core
or if you use yarn
yarn add @aex/core
-
Use
@http
to enable a class with web abilityimport { Aex, http } from "@aex/core"; class HelloAex { private name = "Alice"; constructor(name: string) { this.name = name; } @http("*", "*") public all(_req: any, res: any, _scope: any) { res.end("Hello from " + this.name + "!"); } }
-
Create an aex instance
const aex = new Aex();
-
Push your class to aex with parameters if you
// push your handler into aex with constructor parameters in order aex.push(HelloAex, "Eric");
-
Prepare aex enviroment
aex.prepare();
-
Start aex web server
aex.start(8080).then(); // or await aex.start(8080);
Aex is written in typescript, but it can be very well used with javascript.
You can click here to get the starter for javascript developers.
it is located at: https://github.com/aex-ts-node/aex-babel-node-js-starter; You can refer it every time your want to create a new aex project.
The aex object has many functions for middlewares and classes.
They are:
- use To add a middleware
- push To push a class
- prepare To prepare the server
- start To start the server
Add middlewares to aex, see detailed explanations in middlewares. These middlewares will be global to all http requests.
push a controller class to aex, it takes at lease a class and its constructor's parameters followed by the class.
- aClass: a class prototype.
- args: takes the rest arguments for the class constructor
aex.push(HelloAex);
//or
aex.push(HelloAex, parameter1, parameter2, ..., parameterN);
// will be invoked as `new HelloAlex(parameter1, parameter2, ..., parameterN)`
prepare
is used here to init middlewares and request handlers written within use classes after these classes pushed to the aex
instance. It takes no parameter and return the aex
instance. so you can invoke the start
function.
Aex introduces no MVC but the Web Straight Line to relect the flows how http requests are processed.
await aex.prepare().start();
// or
aex
.prepare()
.start()
.then(() => {
// further processing
});
start
function is used to bootstrap the server with cerntain port. It takes three parameters:
port
the port taken by the web server, defaults to 3000ip
the ip address where the port binds to, defaults to localhostprepare
prepare middlewares or not, used when middlewares are not previously prepared
Aex is simplified by decorators, so you should be familiar with decorators to fully utilize aex.
Decorators will be enriched over time. Currently aex provides the following decorators:
- HTTP method decorators (
@http
,@get
,@post
) - Data parsing decorators (
@formdata
,@query
,@body
) - Static file serving decorators (
@serve
) - Template decorators (
@template
) - Session management decorators (
@session
) - Data filtering and validation decorators (
@filter
) - Error definition decorators (
@error
) - Custome middleware decorators (
@inject
) - API development decorators (
@rest @api
) - Event messaging decorators (
@listen
)
Classically, the node original library handles HTTP requests with a request and response separately. For async's sake, node also provide a callback to further processing the request. That is to say, node has the original http handler calllback function parameter mode like this.
function classicHTTPHandler(req, res, next)
We can call this mode the classic mode. This mode is a very basic parameter passing method, it can only passes the request, the response and the callback function when async is not promised.
It has at least two weakness:
- Not promise-based, not friendly with async/await functions.
- Don't have a proper place to carry generated data by middlewares when going through the Web Straight Line.
On solve problem one, someone created a wrong mode: The Onoin Mode. Which turns lined lists with stacks. Its function parameter mode looks like this.
async function onoinHTTPHandler(req, res, next)
It enabled promise and incorrectly kept the callback function next
.
And we can call this mode the onoin mode.
As this project point out, the web processing patthen is called the web straight line. It goes every phrase one by one orderly and sequentially, not like a stack.
This causes rewriting of every middleware ever created originally supporting the classic mode.
It is obvious a waste of effort and discouragment for developers to maintain two copy middlewares for one purpose. Fortunately this wrong mode never dominates.
By the time of this writing, this mode is still relatively small due to it's incorrectness.
The correct way to asynchronize the classic mode is to remove the next callback. And in order to fixed weakness two, we can introduce a new parameter for data carriage. In aex, it introduces the function parameter mode like this.
async function aexHTTPHandler(req, res, scope) {
res.end();
scope.body;
scope.inner;
}
Which we can call it the promis based mode or the aex mode, first introduced by the aex web framework project.
But this mode is still some time inconvient when you don't have to use request , response.
So to compact them into one variable may be more efficiently, then we have the compatc mode.
The compact mode is an optimization for some cases when you don't use the parameters in an ordered way. For example, you are writing a middleware only needs to process the scoped data. You don't need request, response to show up at all. then you can use this mode. Its function parameter mode looks like this.
async function compactHTTPHandler(ctx) {
// ctx.request
// ctx.response
ctx.scope;
}
So you can ignore the req
, res
parameters with compact mode when necessary.
The compact mode and the promise-based mode/ the aex mode are the recommended modes in aex.
This decorators are the most basic decorators, all decorators should follow them. They are
@http
, @get
, @post
.
@http
is the generic http method decorator. @get
, @post
are the shortcuts for @http
;
The @http
decorator defines your http handler with a member function.
The member methods are of IAsyncMiddleware
type.
@http
takes two parameter:
- http method name(s)
- url(s);
- use Compact mode?
You can just pass url(s) if you use http GET
or POST
method only or you can use @get
or @post
.
You can use @compact
to simplify @http
decorator without fill compact mode.
Here is how your define your handlers.
import { http, get, post } from "@aex/core";
class User {
// Classic http parameters passing
@http("get", ["/profile", "/home"])
profile(req, res, scope) {}
// Compact Mode parameters passing
@http(["get", "post"], "/user/login", true)
login(ctx) {
const { req, res, scope } = ctx;
}
// Compact Mode using @compact
@compact(["get", "post"], "/user/login")
login(ctx) {
const { req, res, scope } = ctx;
}
@http("post", "/user/logout")
logout() {}
@http("/user/:id")
info() {}
@http(["/user/followers", "/user/subscribes"])
followers() {}
@get(["/user/get", "/user/gets"])
rawget() {}
@post("/user/post")
rawpost() {}
}
These decorators will parse all data passed thought the HTTP protocol.
They are @formdata
, @query
, @body
.
@formdata
can parsemulit-part
formdata such as files intoscope.files
and other formdata intoscope.body
. When parsed, you can retrieve yourmulti-part
formdata fromscope.files
,scope.body
.@query
can parse url query intoscope.query
.@body
can parse some simple formdata intoscope.body
.
Decorator @formdata
is a simplified version of node package busboy
for aex
, only the headers
options will be auto replaced by aex
. So you can parse valid options when necesary.
All uploaded files are in array format, and it parses body as well.
import { http, formdata } from "@aex/core";
class Formdata {
protected name = "formdata";
@http("post", "/file/upload")
@formdata()
public async upload(_req: any, res: any, scope: any) {
const { files, body } = scope;
// Access your files
const uploadedSingleFile = files["fieldname1"][0];
const uploadedFileArray = files["fieldname2"];
// Access your file info
uploadedSingleFile.temp; // temporary file saved
uploadedSingleFile.filename; // original filename
uploadedSingleFile.encoding; // file encoding
uploadedSingleFile.mimetype; // mimetype
// Access none file form data
const value = body["fieldname3"];
res.end("File Uploaded!");
}
}
Decorator @body provides a simple way to process data with body parser. It a is a simplified version of node package body-parser.
It takes two parameters:
- types in ["urlencoded", "raw", "text", "json"]
- options the same as body-parser take.
then be parsed into scope.body
, for compatibility req.body
is still available.
Simply put:
@body("urlencoded", { extended: false })
Full example
import { http, body } from "@aex/core";
class User {
@http("post", "/user/login")
@body("urlencoded", { extended: false })
login() {
const [, , scope] = arguments;
const { body } = scope;
}
@http("post", "/user/logout")
@body()
login() {
const [, , scope] = arguments;
const { body } = scope;
}
}
Decorator @query will parse query for you. After decorated with @query
you will have scope.query
to use. req.query
is available for compatible reasion, but it is discouraged.
class Query {
@http("get", "/profile/:id")
@query()
public async id(req: any, res: any, _scope: any) {
// get /profile/111?page=20
req.query.page;
// 20
}
}
Aex provides @serve
decorator and its alias @assets
for static file serving.
Due to
static
is taken as a reserved word for javascript,static
is not supported.
They take only one parameter:
- url: the base url for your served files.
It is recursive, so place used with caution, don't put your senstive files under that folder.
import { serve } from "@aex/core";
class StaticFileServer {
protected name = "formdata";
@serve("/assets")
public async upload() {
// All your files and subdirectories are available for accessing.
return resolve(__dirname, "./fixtures");
}
@assets("/assets1")
public async upload() {
// All your files and subdirectories are available for accessing.
return resolve(__dirname, "./fixtures");
}
}
Aex provides @template
decorator for you to customize your template engine.
with @template
decorator, you can use multiple template engines within one class.
Decorator @template
takes four parameters:
- init template initializing function that returns an template engine
- path where the templates are located
- ext file extension if necessary, defaults to html
- options options if necessary
Function init
should return an template engine that has the render
function.
the render
function then can be used by the res
object passed by the middleware.
If the engine has no render function or the parameters are vary from the required IInitFunction
interface, you should return a new engine with a compatible render
function.
When the engine is returned, it will be added to scope
, so you can access it when necessary.
Hence we have two ways to render a template file:
- use
res.render
- use
scope.engine
The following is an example on how to use @template
decorator:
- Template name
nunjucks
- with compatible
render
function
class Template {
@http("/one")
@template(
(path) => {
const loader = new nunjucks.FileSystemLoader([path], {});
const env = new nunjucks.Environment(loader, {
autoescape: false,
});
return env;
},
resolve(__dirname, "./views"),
)
public async name(_: any, res: any) {
// access using res.render
res.render("index.html", { hello: "Hello" });
}
}
- Template name:
pug
- with
render
function rewritten
class Template {
@http("/pug")
@template(
(path) => {
const engine: any = {};
// engine render function rewrite
engine.render = function (name: string, data: any) {
let file = name;
if (!existsSync(name)) {
if (!existsSync(resolve(path, name))) {
throw new Error("File Not Found: " + resolve(path, name));
} else {
file = resolve(path, name);
}
}
return pug.renderFile(file, data);
};
return engine;
},
resolve(__dirname, "./views"),
)
public async name2(_: any, res: any, scope: any) {
// access using scope.engine
res.end(scope.engine.render("index.pug", { hello: "Hello3" }));
}
}
Be sure to remember when using
scope.engine
, you needres.end
too.
Due to the flexibility aex provides, you can mix then with one class with ease.
class Template {
@http("/one")
@template(
(path) => {
const loader = new nunjucks.FileSystemLoader([path], {});
const env = new nunjucks.Environment(loader, {
autoescape: false,
});
return env;
},
resolve(__dirname, "./views"),
)
public async name(_: any, res: any) {
// access using res.render
res.render("index.html", { hello: "Hello" });
}
@http("/pug")
@template(
(path) => {
const engine: any = {};
// engine render function rewrite
engine.render = function (name: string, data: any) {
let file = name;
if (!existsSync(name)) {
if (!existsSync(resolve(path, name))) {
throw new Error("File Not Found: " + resolve(path, name));
} else {
file = resolve(path, name);
}
}
return pug.renderFile(file, data);
};
return engine;
},
resolve(__dirname, "./views"),
)
public async name2(_: any, res: any, scope: any) {
// access using scope.engine
res.end(scope.engine.render("index.pug", { hello: "Hello3" }));
}
}
Aex provides @session
decorator for default cookie based session management.
Session in other format can be realized with decorator @inject
.
Decorator @session
takes a store as the parameter. It is an object derived from the abstract class ISessionStore. which is defined like this:
export declare abstract class ISessionStore {
abstract set(id: string, value: any): any;
abstract get(id: string): any;
abstract destroy(id: string): any;
}
aex
provides two default store: MemoryStore
and RedisStore
.
RedisStore
can be configurated by passing options through its constructor. The passed options is of the same to the function createClient
of the package redis
. You can check the option details here
For MemoryStore
, you can simply decorate with @session()
.
For RedisStore
, you can decorate with an RedisStore as @session(redisStore)
. Be sure to keep the variable redisStore global, because sessions must share only one store.
// Must not be used @session(new RedisStore(options)).
// For sessions share only one store over every request.
// There must be only one object of the store.
const store = new RedisStore(options);
class Session {
@post("/user/login")
@session()
public async get() {
const [, , scope] = arguments;
const { session } = scope;
session.user = user;
}
@get("/user/profile")
@session()
public async get() {
const [, , scope] = arguments;
const { session } = scope;
const user = session.user;
res.end(JSON.stringify(user));
}
@get("/user/redis")
@session(store)
public async get() {
const [, , scope] = arguments;
const { session } = scope;
const user = session.user;
res.end(JSON.stringify(user));
}
}
Share only one store object over requests.
Use the following code if you encouter session cycle lags/deadth in some environment:
if (scope.session.save) {
await scope.session.save();
}
Aex provides @filter
to filter and validate data for you.
Decorator @filter
will filter body
, params
and query
data for you, and provide fallbacks respectively for each invalid data processing.
If the filtering rules are passed, then you will get a clean data from scope.extracted
.
You can access scope.extracted.body
, scope.extracted.params
and scope.extracted.query
if you filtered them.
But still you can access req.body
, req.query
, req,params
after filtered.
Reference node-form-validator for detailed usage.
class User {
private name = "Aex";
@http("post", "/user/login")
@body()
@filter({
body: {
username: {
type: "string",
required: true,
minLength: 4,
maxLength: 20
},
password: {
type: "string",
required: true,
minLength: 4,
maxLength: 64
}
},
fallbacks: {
body: async(error, req, res, scope) {
res.end("Body parser failed!");
}
}
})
public async login(req: any, res: any, scope: any) {
// req.body.username
// req.body.password
// scope.extracted.body.username
// scope.extracted.body.password
// scope.extracted is the filtered data
}
@http("get", "/profile/:id")
@body()
@query()
@filter({
query: {
page: {
type: "numeric",
required: true
}
},
params: {
id: {
type: "numeric",
required: true
}
},
fallbacks: {
params: async function (this: any, _error: any, _req: any, res: any) {
this.name = "Alice";
res.end("Params failed!");
},
}
})
public async id(req: any, res: any, _scope: any) {
// req.params.id
// req.query.page
}
}
Aex provides @error
decorator for error definition
Decorator @error
will generate errors for you.
Reference errorable for detailed usage.
@error
take two parameters exactly what function Generator.generate
takes.
Besides you can add lang
attribut to @error
to default the language, this feature will be automatically removed by aex
when generate errors.
With the lang attribute, you can new errors without specifying a language every time throw/create an error;
class User {
@http("post", "/error")
@error({
lang: "zh-CN",
I: {
Love: {
You: {
code: 1,
messages: {
"en-US": "I Love U!",
"zh-CN": "我爱你!",
},
},
},
},
Me: {
alias: "I",
},
})
public road(_req: any, res: any, scope: any) {
const [, , scope] = arguments;
const { error: e } = scope;
const { ILoveYou } = e;
throw new ILoveYou("en-US");
throw new ILoveYou("zh-CN");
throw new ILoveYou(); // You can ignore language becuase you are now use the default language.
res.end("User Error!");
}
}
Aex provides @inject
decorator for middleware injection.
@inject
decrator takes two parameters:
- injector: the main injected middleware for data further processing or policy checking
- fallback?: optional fallback when the injector fails and returned
false
- useCompactMode?:
true
to use compact mode.
class User {
private name = "Aex";
@http("post", "/user/login")
@body()
@inject(async () => {
req.session = {
user: {
name: "ok"
}
};
})
@inject(async function(this:User, req, res, scope) {
this.name = "Peter";
req.session = {
user: {
name: "ok"
}
};
})
@inject(async function compactMode(this:User, ctx) {
this.name = "Peter";
ctx.req.session = {
user: {
name: "ok"
}
};
}, , true)
@inject(async function(this:User, req, res, scope) => {
this.name = "Peter";
if (...) {
return false
}
}, async function fallback(this:User, req, res, scope){
// some fallback processing
res.end("Fallback");
})
@inject(async function compactedMiddleware(this:User, ctx) => {
this.name = "Peter";
if (...) {
return false
}
}, async function compactedFallback(this:User, ctx){
// some fallback processing
ctx.res.end("Fallback");
}, true)
public async login(req: any, res: any, scope: any) {
// req.session.user.name
// ok
...
}
}
Aex can route messages between Objects with an Aex instance.
You only need to prefix an member function with @listen
and pass a event
name to it, then you are listening to the event from all aex inner http handling class now.
A simple example is like the following:
class Listen {
private name: string;
constructor(name?: string) {
this.name = name || "";
}
@listen("echo")
public echo(emitter: EventEmitter, ...messages: any[]) {
emitter.emit("echoed", messages[0]);
}
@get("/")
public async get(_req: any, res: any, scope: Scope) {
scope.emitter.on("echoed1", (mesasge) => {
res.end(mesasge + " from " + this.name);
});
scope.emitter.emit("echo1", "Hello");
}
}
class Listen1 {
@listen("echo1")
public echo1(emitter: EventEmitter, ...messages: any[]) {
emitter.emit("echoed1", "echoed1 " + messages[0]);
}
}
const emitter = new EventEmitter();
const aex = new Aex(emitter);
let port: number = 0;
aex.push(Listen, "Nude");
aex.push(Listen1);
aex.prepare().start();
This example shows that
- The
Listen
class listens to an event calledecho
, within this handler, it sends a new event calledechoed
;if you listen to this event with the emitter, you will have notification for this event. - The
Listen
class also can handle http request to url/
, it then emitecho1
event which will invokeListen1
's listener afterListen1
pushed to the same Aex object. - The
Listen1
class listens to an event calledecho1
, within this handler, it emits a new event calledechoed1
; if you listen to this event with the emitter, you will have notification for this event.
if we only need to send messages between objects, just use emitter to send messages:
emitter.emit("echo", "Hello Aex!");
emitter is an Buildin object from node.
Aex only simplifies this usage between classes, the behavior is not changed, you can refer node's EventEmitter for further information.
Event listeners of a class should not be http handlers of the class, because they process different things.
-
Create an Aex instance
const aex = new Aex();
-
Create a Router
const router = new Router();
-
Setup the option for handler
router.get("/", async () => { // request processing time started console.log(scope.time.stated); // processing time passed console.log(scope.time.passed); res.end("Hello Aex!"); });
-
Use router as an middleware
aex.use(router.toMiddleware());
-
Start the server
const port = 3000; const host = "localhost"; const server = await aex.start(port, host); // server === aex.server
-
Create a
WebSocketServer
instanceconst aex = new Aex(); const server = await aex.start(); const ws = new WebSocketServer(server);
-
Get handler for one websocket connection
ws.on(WebSocketServer.ENTER, (handler) => { // process/here });
-
Listen on user-customized events
ws.on(WebSocketServer.ENTER, (handler) => { handler.on("event-name", (data) => { // data.message = "Hello world!" }); });
-
Send message to browser / client
ws.on(WebSocketServer.ENTER, (handler) => { handler.send("event-name", { key: "value" }); });
-
New browser/client WebSocket object
const wsc: WebSocket = new WebSocket("ws://localhost:3000/path"); wsc.on("open", function open() { wsc.send(""); });
-
Listen on user-customized events
ws.on("new-message", () => { // process/here });
-
Sending ws message in browser/client
const wsc: WebSocket = new WebSocket("ws://localhost:3000/path"); wsc.on("open", function open() { wsc.send( JSON.stringify({ event: "event-name", data: { message: "Hello world!", }, }), ); });
-
Use websocket middlewares
ws.use(async (req, ws, scope) => { // return false });
Middlewares/Handlers are defined like this:
- Classic one
export type IAsyncMiddleware = (
req: Request,
res: Response,
scope?: Scope,
) => Promise<boolean | undefined | null | void>;
- Compacted Mode
export interface IACompactedAsyncMiddeleWare {
req: IRequest;
res: IResponse;
scope?: Scope;
}
export type ICompactedAsyncMiddleware = (
context: IACompactedAsyncMiddeleWare,
) => Promise<boolean | undefined | null | void>;
They return promises, so they must be called with await
or .then()
.
Global middlewares are effective all over the http request process.
They can be added by aex.use
function.
aex.use(async () => {
// process 1
// return false
});
aex.use(async () => {
// process 2
// return false
});
// ...
aex.use(async () => {
// process N
// return false
});
Return
false
in middlewares will cancel the whole http request processing
It normally happens after ares.end
Handler specific middlewares are effective only to the specific handler.
They can be optionally added to the handler option via the optional attribute middlewares
.
the middlewares
attribute is an array of async functions of IAsyncMiddleware
.
so we can simply define handler specific middlewares as follows:
router.get(
"/",
async () => {
res.end("Hello world!");
},
[
async () => {
// process 1
// return false
},
async () => {
// process 2
// return false
},
// ...,
async () => {
// process N
// return false
},
],
);
Websocket middlewares are of the same to the above middlewares except that the parameters are of different.
type IWebSocketAsyncMiddleware = (
req: Request,
socket: WebSocket,
scope?: Scope,
) => Promise<boolean | undefined | null | void>;
The Websocket Middlewares are defined as IWebSocketAsyncMiddleware
, they pass three parameters:
- the http request
- the websocket object
- the scope object
THe middlewares can stop websocket from further execution by return false
The node system http.Server
.
Accessable through aex.server
.
const aex = new Aex();
const server = await aex.start();
expect(server === aex.server).toBeTruthy();
server.close();
Aex provides scoped data for global and local usage.
A scope object is passed by middlewares and handlers right after req
, res
as the third parameter.
It is defined in IAsyncMiddleware
as the following:
async () => {
// process N
// return false
};
the scope
variable has 8 native attributes: orm
, engine
, time
, outer
/global
, inner
/local
, query
, params
, body
, error
, debug
The orm
attribute contains the orm objects, such as prisma orm.
The engine
attribute contains the html/xml template objects, such as nunjunks .
The time
attribute contains the started time and passed time of requests.
The outer
/global
attribute is to store general or global data.
The inner
/local
attribute is to store specific or local data.
The query
attribute is to store http query.
The body
attribute is to store http body.
The params
attribute is to store http params.
The error
attribute is to store scoped errors.
The debug
attribute is to provide handlers with the debugging ability.
scope.time.started;
// 2019-12-12T09:01:49.543Z
scope.time.passed;
// 2019-12-12T09:01:49.543Z
The outer
/global
and inner
/local
variables are objects used to store data for different purposes.
You can simply assign them a new attribute with data;
scope.inner.a = 100;
scope.local.a = 100;
scope.outer.a = 120;
scope.global.a = 120;
debug
is provided for debugging purposes.
It is a simple import of the package debug
.
Its usage is of the same to the package debug
, go debug for detailed info.
Here is a simple example.
async () => {
const { debug } = scope;
const logger = debug("aex:scope");
logger("this is a debugging info");
};
// scope.outer = {}; // Wrong operation!
// scope.inner = {}; // Wrong operation!
// scope.global = {}; // Wrong operation!
// scope.local = {}; // Wrong operation!
// scope.time = {}; // Wrong operation!
// scope.query = {}; // Wrong operation!
// scope.params = {}; // Wrong operation!
// scope.body = {}; // Wrong operation!
// scope.error = {}; // Wrong operation!
// scope.debug = {}; // Wrong operation!
// scope.time.started = {}; // Wrong operation!
// scope.time.passed = {}; // Wrong operation!
Helpers are special middlewares with parameters to ease some fixed pattern with web development.
They must work with decorator @inject
.
The first aviable helper is paginate
.
Helper paginate
can help with your pagination data parsing and filtering. It gets the correct value for you, so you can save your code parsing and correcting the pagination data before using them.
paginate
is function takes two parameter:
-
limit
is the default value of limitation for pagination, if the request is not specified. This function defaults it to20
, so you your request doesn't specific a value tolimit
, it will be assigned20
. -
type
is the type data you use for pagination, it can bebody
,params
,query
.query
is the default one. Before usepaginate
, you must parsed your data. For if you usebody
for pagination, normally your reuqest handlers should be deocrated with@body
. Becuase params are parsed internally, using params needs no docrator. The data to be parsed must contain two parameters which should be named with :page
,limit
. for typequery
, pagination data can be :list?page=2&limit=30
;
After parsing, scope
will be added a attribute pagination
, which is a object have three attribute: page
, limit
, offset
. so you can simply extract them with
const {
pagination: { page, limit, offset },
} = scope;
Here is how to use helper paginate
.
class Paginator {
@http('/page/query')
@query()
@inject(paginate(10, 'query'))
async public pageWithQuery() {
const [, , scope] = arguments;
const {pagination: {page, limit, offset}} = scope;
...
}
@http('/page/body')
@body()
@inject(paginate(10, 'body'))
async public pageWithBody() {
const [, , scope] = arguments;
const {pagination: {page, limit, offset}} = scope;
...
}
@http('/page/params/:page/:limit')
@body()
@inject(paginate(10, 'body'))
async public pageWithParams() {
const [, , scope] = arguments;
const {pagination: {page, limit, offset}} = scope;
...
}
}
Aex provide a way for express middlewares to be translated into Aex middlewares.
You need just a simple call to toAsyncMiddleware
to generate Aex's async middleware.
const oldMiddleware = (_req: any, _res: any, next: any) => {
// ...
next();
};
const pOld = toAsyncMiddleware(oldMiddleware);
aex.use(pOld);
You should be cautious to use express middlewares. Full testing is appreciated.
npm install
npm test
You can very easily integrate other libraries into aex. You can use global or local middlewares to add them to aex.
const aex = new Aex();
prisma = new PrismaClient();
await prisma.$connect();
// Construct a middleware to add prisma to aex's middleware carrier, the scope variable.
const prismaAttachingMiddleware = async function (req, res, scope) {
Object.defineProperty(scope, "prisma", {
value: prisma,
});
};
// The Global Way
aex.use(prismaAttachingMiddleware);
// The Local Way
class Hello() {
@http("/")
@inject(prismaAttachingMiddleware)
public async index(req, res, scope) {
// Access prisma from scope
let orm = scope.prisma;
}
}
Aex follows a general versioning called Effective Versioning.
Aex supports human equality in law of rights only and supports human diversity in other area. Humans will never be equal in most area which is law of nature.
MIT