UNPKG

18.5 kBMarkdownView Raw
1# lynx
2
3lynx is a NodeJS framework for Web Development, based on decorators and the async/await support.
4
5The idea is to enforce code maintainability and readability enforcing a precise project structure, **decorators** and completely remove the callback (or promises) nightmare leaning on the new **async/await** supports, available in the new versions of NodeJS.
6
7Lynx is influenced by the Java Spring framework, trying to bring a more enterprise-level environment to NodeJS.
8
9## Libraries
10
11Lynx is founded by state-of-the-art libraries. It uses:
12
13- **[ExpressJS](http://expressjs.com/)** for the management of routes;
14- **[nunjucks](https://mozilla.github.io/nunjucks/)** as template engine;
15- **[TypeORM](http://typeorm.io/)** as ORM to the database;
16- **[JWT](https://jwt.io/)** to enable token authentication;
17- **[multer](https://github.com/expressjs/multer)** to manage file upload;
18- **[nodemailer](https://nodemailer.com)** to send emails;
19- **[joi](https://github.com/hapijs/joi)** to validate the requests;
20- **[jimp](https://github.com/oliver-moran/jimp)** to perform image resizing and other operations.
21
22## Out-Of-The-Box Features
23
24With Lynx, you will have the following functionality out of the box:
25
26- user management, and low level function for login, registration, authorization and authentication;
27- media upload management, file upload and retrieving, on a virtual-folder environment;
28- multi-language is a first class citizen! You can use new nunjucks filter to enable multi-language on the template engine, but also during fields validation!
29- datatables, directly integrated on the template engine, with pagination, filtering and ordering (please use the `lynx-datatable` module)
30
31## Installation
32
33```
34npm install lynx-framework
35```
36
37## Lynx module structure
38
39A Lynx module shall be formed by different folders:
40
41```
42.
43├── controllers
44│   ├── backoffice
45│   │   └── main.controller.ts
46│   └── main.controller.ts
47├── entities
48├── index.ts
49├── libs
50├── locale
51│   ├── en.json
52│   └── it.json
53├── middlewares
54│   └── always.middleware.ts
55├── public
56├── templating
57└── views
58 └── main.njk
59```
60
61- The `controllers` folder shall contain (with subfolder support) any controllers.
62- The `entities` folder shall contain any entities, that will be automatically mapped with `TypeORM`.
63- The `libs` folder shall contain any additional libraries or utility functions, that can be used by controllers and middlewares.
64- The `local` folder shall contains localization file formatted as key-value JSON file.
65- The `middlewares` folder shall contain (with subfolder support) any middleware.
66- The `public` folder shall contain all the public resources, such as images, css and so on.
67- The `templating` folder shall contain filters and functions to enchant the templating system.
68- The `view` folder shall contain the nunjucks templates. An `emails` subfolder, containing the email templates, is recommended.
69
70The `index.ts` should be the entry point of the module, defining a class that implements the `BaseModule` abstract class. For semplicity, also a `SimpleModule` class can be used, in order to define only the folders needed by the module.
71
72The project structure can be customized with different folder names, simply editing the module class. Otherwise, this structure is strictly recommended.
73
74## Lynx application
75
76A Lynx application is composed by at least one module, and an entry point of the standard node application.
77
78To start a Lynx application, it is necessary to instantiate a Lynx `App` object. For example, the `index.ts` file can be:
79
80```
81import { App, ConfigBuilder } from "lynx-framework/app";
82import AppModule from "./modules/app";
83
84const port = Number(process.env.PORT) || 3000;
85
86const app = new App(new ConfigBuilder(__dirname, false).build(), [new AppModule()]);
87app.startServer(port);
88```
89
90Any Lynx configuration, such as database connection, token secrets, folders and so on, can be customized using the `ConfigBuilder` object.
91Any controllers, middlewares and entities of any modules will be automatically loaded by the Lynx app.
92
93## Controllers
94
95A controller defines a set of endpoints. Any endpoint responds to a specific
96path and HTTP verb, and can generate an HTML or JSON response.
97Any controller shall extends the `BaseController` class, in order to inherit a lot of
98utility methods.
99It is possible to define only ONE controller for each file, and the class shall be `export default`.
100Moreover, the file should be named as `controllerName.controller.ts`.
101
102The minimum configuration of a controller is the following:
103
104```
105import { Route } from "lynx-framework/decorators";
106import BaseController from "lynx-framework/base.controller";
107
108@Route("/myController/path")
109export default class MyController extends BaseController {
110 ...
111}
112```
113
114To define endpoints, it is necessary to decor a method. There is a decorator for each HTTP verb (GET, POST, etc..). For example:
115
116```
117 ...
118 @GET('/helloWorld')
119 async helloWorld() {
120 return "Hello, world!";
121 }
122```
123
124the method `helloWorld` will be executed for any `GET` request to `/myController/path/helloWorld`.
125
126### Method decorators
127
128#### `GET(path)`, `POST(path)`, `PUT(path)`, `PATCH(path)`, `DELETE(path)`
129
130These decorators map the method to the chosen HTTP verb with the specified path.
131Moreover, `path` can contains path parameters, for example `/authors/:id/posts`. The path parameters will be injected in the method as arguments:
132
133```
134 @GET('/authors/:id/posts/:secondParameter')
135 async doubleParameters(id:Number, secondParameter:String) {
136 ...
137 }
138```
139
140Since Lynx is based on Express, more information about the url parameters can be found [here](https://expressjs.com/en/guide/routing.html).
141
142**IMPORTANT** these decorators shall be put just before the method definition, and as last decorator used to the method.
143
144### `API()`
145
146The `API` decorator enforce the serialization of the returned object to JSON. This feature is very useful to build an API.
147In this case, the returned object will be added inside the following standard return object:
148
149```
150 {
151 "success": true/false,
152 "data": "returned object serialized"
153 }
154```
155
156If the method returns a boolean value, the return object will be:
157
158```
159 {
160 "success": returned value
161 }
162```
163
164For more information about the object serialization, please check the [Entity chapter]().
165
166### `MultipartForm()`
167
168This decorator simply allows the MultipartForm in post request. It is essential to enable the automatic file upload system.
169
170### `Name(name)`
171
172This decorator allows to set a name to the route. So, it is possible to recall this route in a very simple way.
173The route name is used by any `route` functions.
174
175### `Verify(function)`
176
177Add to the decorated method a verification function that will be executed BEFORE the route.
178The function must NOT be an async function, and it shell return a boolean value. If true is returned, the method is then executed. This method is fundamental to implement authorization to a single endpoint.
179NOTE: the function shall NOT be a class method, but a proper Typescript function.
180Example:
181
182```
183function alwaysDeny(req, res) {
184 return false;
185}
186...
187@Verify(alwaysDeny)
188@GET("/unreachable")
189async someMethod() {
190 ...
191}
192```
193
194### `AsyncVerify(function)`
195
196Add to the decorated method a verification function that will be executed BEFORE the route.
197The function MUST BE an async function, and it shell return a boolean value. If true is returned, the method is then executed. This method is fundamental to implement authorization to a single endpoint.
198NOTE: the function shall NOT be a class method, but a proper Typescript function.
199
200> This method is available from version 0.5.5
201
202Example:
203
204```
205async function alwaysDeny(req, res) {
206 return false;
207}
208...
209@AsyncVerify(alwaysDeny)
210@GET("/unreachable")
211async someMethod() {
212 ...
213}
214```
215
216### `IsDisabledOn(function)`
217
218Add to the decorated method a verification function that will be executed BEFORE the route.
219The function shall return a boolean value and it is evaluated during the server startup.
220If the function return true, the decorated method is ignored and is not added to the current controller.
221
222> This method is available from version 1.1.5.
223
224Example:
225
226```
227function disableOnProduction() {
228 return isProduction == true;
229}
230...
231@IsDisabledOn(disableOnProduction)
232@GET("/test")
233async testMethod() {
234 ...
235}
236```
237
238### `Body(name, schema)`
239
240The `Body` decorator inject the request body as a parameter of the decorated method. The body object
241is automatically wrapped inside a `ValidateObject`, that is verified using a [JOI schema](https://github.com/hapijs/joi).
242Example:
243
244```
245import { ValidateObject } from "lynx-framework/validate-object";
246import * as Joi from "joi";
247
248const loginSchema = Joi.object().keys({
249 email: Joi.string()
250 .email()
251 .required()
252 .label("{{input_email}}"), //I can use a localized string!
253 password: Joi.string()
254 .required()
255 .min(4)
256 .regex(/^[a-zA-Z0-9]{3,30}$/)
257 .label("{{input_password}}") //I can use a localized string!
258});
259
260...
261
262@Body("d", loginSchema)
263@POST("/login")
264async performLogin(
265 d: ValidateObject<{ email: string; password: string }>
266) {
267 if (!d.isValid) {
268 //d.errors contains localized errors!
269 return false;
270 }
271 let unwrapped = d.obj; //I can use unwrapped.email and unwrapped.password!
272 ...
273}
274```
275
276Starting from version `0.5.8`, a new builder class can be used to create Joi schemas.
277The previous example can be simplified as follows:
278
279```
280import { ValidateObject, SchemaBuilder } from "lynx-framework/validate-object";
281
282...
283
284const loginSchema = new SchemaBuilder()
285 .email("email")
286 .withLabel("{{input_email}}")
287 .password("password")
288 .withLabel("{{input_password}}")
289 .build();
290
291...
292
293@Body("d", loginSchema)
294@POST("/login")
295async performLogin(
296 d: ValidateObject<{ email: string; password: string }>
297) {
298 if (!d.isValid) {
299 //d.errors contains localized errors!
300 return false;
301 }
302 let unwrapped = d.obj; //I can use unwrapped.email and unwrapped.password!
303 ...
304}
305```
306
307### Advanced
308
309#### Accessing the original `req` and `res`
310
311When an endpoint method is called, the last two arguments always are the original `req` and `res` objects.
312The `req` object has also the `user` and `files` properties (it is a _Lynx Request Object_).
313The use of `res` object is discouraged, in favor of a standard returned object from the endpoint method.
314Example:
315
316```
317@GET("/endpoint/:id")
318async myEndpoint(id:Number, req: Request, res: Response) {
319 ...
320}
321```
322
323#### `postConstructor`
324
325It is possible to override the `postConstructor` methods that will be called after the creation of the controller. This method is `async`, so it is possible to perform asynchronous initialization. Always remember that there will be only ONE instance of any controller in a Lynx application.
326
327## Enhancements to the Nunjucks engine
328
329### `tr` filter
330
331The `tr` filter automatically localize a string. Usage:
332
333```
334<button type="submit" class="btn btn-primary px-4">{{ "button_login" | tr }}</button>
335```
336
337The `button_login` shall be a property in the JSON localized file.
338
339### `json` filter
340
341The `json` filter automatically format an object or variable to JSON.
342
343### `format` filter
344
345The `format` filter format a number to a string, with a fixed number of decimal digits (default: 2).
346Usage:
347
348```
349<span class="price">€ {{ price | format }}</span>
350<span class="integer_number">{{ myNumber | format(0) }}
351```
352
353### `date` filter
354
355The `date` filter format a date to a string, using the `moment`. The default format will use the
356`lll` format, but it is possible to override this behavior.
357Usage:
358
359```
360<span class="date">€ {{ data.createdAt | date }}</span>
361<span class="my_date_custom">{{ data.createdAt | date("YYYY-MM-DD") }}
362```
363
364### `route` global function
365
366The `route` function compile a route name to an url with the given parameters.
367If an url is used instead of a route name, the url is still compiled with the given parameters.
368Usage:
369
370```
371<a href="{{route('forgot_password')}}" class="btn btn-link px-0">...</a>
372```
373
374To set the name of a route, use the `Name` decorated to the chosen method.
375
376### `old` global function
377
378The `old` function is used to retrieve the latest value of a form. It can be used to retrieve the value of an input while performing server side form validation. It is also possible to specify a default value.
379Usage:
380
381```
382<input type="email" name="email" class="form-control" value="{{old('email')}}">
383```
384
385### `currentHost` global function
386
387The `currentHost` function is used to retrieve the current server host. This can be used, with the `route` function, to generate an absolute
388url (for example, needed to generate an url for an email).
389
390### Add custom filters and functions to the template engine.
391
392It is possible to add new custom filters and functions using the `TemplateFilter` and `TemplateFunction` decorators.
393In both cases, it is necessary to create a new class for each filter or function, inside the `templating` folder of the module.
394
395NOTE: filters and functions do not need to be defined as `export default class`es as for controllers or entities. Moreover, only once instance for each class will be created (they are managed as "singleton" by Lynx).
396
397#### Custom filter
398
399Example: create a new `currency` filter.
400Create a new `currency.filter.ts` file inside the `templating` folder as follows:
401
402```
403import { TemplateFilter } from 'lynx-framework/templating/decorators';
404import BaseFilter from 'lynx-framework/templating/base.filter';
405
406@TemplateFilter('currency')
407export class CurrencyFiltering extends BaseFilter {
408 filter(val: any, ...args: any[]): string {
409 if (val == undefined) {
410 return val;
411 }
412 //TODO: convert the `val` variable and return the result
413 return val + '€';
414 }
415}
416
417```
418
419### Custom function
420
421Example: create a new `placeholderUrl` function.
422Create a new `placeholder-url.function.ts` file inside the `templating` folder as follows:
423
424```
425import { TemplateFunction } from 'lynx-framework/templating/decorators';
426import BaseFunction from 'lynx-framework/templating/base.function';
427
428@TemplateFunction('placeholderUrl')
429export default class PlaceholderUrlFunction extends BaseFunction {
430 execute(...args: any[]) {
431 let ratio = this.safeGet(args, 0);
432 let text = 'Free';
433 if (!ratio) {
434 ratio = '4:3';
435 } else {
436 text = ratio;
437 }
438 let _ww = (ratio + '').split(':')[0];
439 let _hh = (ratio + '').split(':')[1];
440 let h = ((350 / Number(_ww)) * Number(_hh)).toFixed(0);
441 return 'http://via.placeholder.com/350x' + h + '?text=' + text;
442 }
443}
444
445```
446
447## Custom `API` response
448
449Starting from `1.0.0-rc4`, it is possible to customize the standard response of the `API` tagged routes.
450
451To achieve this feature, it is necessary to implement the `APIResponseWrapper` interface, and set the `apiResponseWrapper` property of your `App` instance.
452By default, the `DefaultResponseWrapper` implementation is used.
453
454## Lynx Modules
455
456Lynx supports custom module to add functionality at the current application. A module act exactly as a legacy Lynx application, with its standard `controllers`, `middlewares`, `entities`, `views`, `locale` and `public` folders.
457Modules shall be loaded at startup time, and shall be injected in the Lynx application constructor:
458
459```
460const app = new App(myConfig, [new DatagridModule(), new AdminUIModule()] as BaseModule[]);
461```
462
463In this example, the Lynx application is created with the `DatagridModule` and the `AdminUIModule` modules.
464
465Modules are the standard to provide additional functionality to the Lynx framework.
466
467## Mail Client
468
469Since day 0, Lynx supports a very simple API to send emails from controllers, with the methods `sendMail` and `sendMailRaw`. Starting from `1.2.0`, this methods are available outside the controller context.
470
471The `App` class define the `mailClient` property of type `MailClient`. This class contains the methods `sendMail` and `sendMailRaw` to respectivly send emails.
472The first method uses the `nunjuks` template system to send emails, both for plain text, html text and subject. The latter is a low level version of the API, directly sending the email body and subject. Both APIs support multiple destination addresses.
473
474The mail client is configured thought the `ConfigBuilder` of the application.
475
476By default, a standard SMTP sender client is used (using the usual NodeMailer library). It is possible to use a custom sender class (that implements the `MailClient` interface) using the `setMailClientFactoryConstructor` method of the `ConfigBuilder`.
477
478## Interceptors
479
480> This feature is available from version 1.1.21
481
482Lynx supports two types of interceptors, in order to manage and edit requests. Currently, _Global Routing Interceptor_ and _Before Perform Response Interceptor_ are supported.
483
484### Global Routing Interceptor
485
486This interceptor can be mounded as an additional `express Router`, and it is executed before any middleware or routes defined by the lynx modules system.
487Seems the interceptor is mounted as a router, it is possible to define a subpath in which it should be executed.
488
489> Warning: usually, it is possible to use a standard middleware to achieve most of the common jobs. Use this interceptor only if it is necessary to execute this function before any middleware.
490
491### Before Perform Response Interceptor
492
493This interceptor is executed just before the `performResponse` method of any Lynx `Response` is executed.
494This interceptor can be useful to override some standard behavior of the framework responses. A typical example is the editing of the final url for a `RedirectResponse`.
495
496### Basic usage
497
498At Jellyfish Solutions, we use this two interceptors in conjunction, in order to manage url rewrite based on the language.
499In this particular case, the language of the user is not set in the usual `Accept-Language` header of the request, but it is a portion of the current url.
500The _Global Routing Interceptor_ is used to remove the language from the request url, and correctly update the usual request language.
501The _Perform Response Interceptor_ is used to add (if necessary) the correct language to a redirect response, without editing the application code.