1 | # lynx
|
2 |
|
3 | lynx is a NodeJS framework for Web Development, based on decorators and the async/await support.
|
4 |
|
5 | The 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 |
|
7 | Lynx is influenced by the Java Spring framework, trying to bring a more enterprise-level environment to NodeJS.
|
8 |
|
9 | ## Libraries
|
10 |
|
11 | Lynx 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 |
|
24 | With 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 | ```
|
34 | npm install lynx-framework
|
35 | ```
|
36 |
|
37 | ## Lynx module structure
|
38 |
|
39 | A 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 |
|
70 | The `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 |
|
72 | The 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 |
|
76 | A Lynx application is composed by at least one module, and an entry point of the standard node application.
|
77 |
|
78 | To start a Lynx application, it is necessary to instantiate a Lynx `App` object. For example, the `index.ts` file can be:
|
79 |
|
80 | ```
|
81 | import { App, ConfigBuilder } from "lynx-framework/app";
|
82 | import AppModule from "./modules/app";
|
83 |
|
84 | const port = Number(process.env.PORT) || 3000;
|
85 |
|
86 | const app = new App(new ConfigBuilder(__dirname, false).build(), [new AppModule()]);
|
87 | app.startServer(port);
|
88 | ```
|
89 |
|
90 | Any Lynx configuration, such as database connection, token secrets, folders and so on, can be customized using the `ConfigBuilder` object.
|
91 | Any controllers, middlewares and entities of any modules will be automatically loaded by the Lynx app.
|
92 |
|
93 | ## Controllers
|
94 |
|
95 | A controller defines a set of endpoints. Any endpoint responds to a specific
|
96 | path and HTTP verb, and can generate an HTML or JSON response.
|
97 | Any controller shall extends the `BaseController` class, in order to inherit a lot of
|
98 | utility methods.
|
99 | It is possible to define only ONE controller for each file, and the class shall be `export default`.
|
100 | Moreover, the file should be named as `controllerName.controller.ts`.
|
101 |
|
102 | The minimum configuration of a controller is the following:
|
103 |
|
104 | ```
|
105 | import { Route } from "lynx-framework/decorators";
|
106 | import BaseController from "lynx-framework/base.controller";
|
107 |
|
108 | @Route("/myController/path")
|
109 | export default class MyController extends BaseController {
|
110 | ...
|
111 | }
|
112 | ```
|
113 |
|
114 | To 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 |
|
124 | the 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 |
|
130 | These decorators map the method to the chosen HTTP verb with the specified path.
|
131 | Moreover, `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 |
|
140 | Since 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 |
|
146 | The `API` decorator enforce the serialization of the returned object to JSON. This feature is very useful to build an API.
|
147 | In 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 |
|
156 | If the method returns a boolean value, the return object will be:
|
157 |
|
158 | ```
|
159 | {
|
160 | "success": returned value
|
161 | }
|
162 | ```
|
163 |
|
164 | For more information about the object serialization, please check the [Entity chapter]().
|
165 |
|
166 | ### `MultipartForm()`
|
167 |
|
168 | This decorator simply allows the MultipartForm in post request. It is essential to enable the automatic file upload system.
|
169 |
|
170 | ### `Name(name)`
|
171 |
|
172 | This decorator allows to set a name to the route. So, it is possible to recall this route in a very simple way.
|
173 | The route name is used by any `route` functions.
|
174 |
|
175 | ### `Verify(function)`
|
176 |
|
177 | Add to the decorated method a verification function that will be executed BEFORE the route.
|
178 | The 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.
|
179 | NOTE: the function shall NOT be a class method, but a proper Typescript function.
|
180 | Example:
|
181 |
|
182 | ```
|
183 | function alwaysDeny(req, res) {
|
184 | return false;
|
185 | }
|
186 | ...
|
187 | @Verify(alwaysDeny)
|
188 | @GET("/unreachable")
|
189 | async someMethod() {
|
190 | ...
|
191 | }
|
192 | ```
|
193 |
|
194 | ### `AsyncVerify(function)`
|
195 |
|
196 | Add to the decorated method a verification function that will be executed BEFORE the route.
|
197 | The 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.
|
198 | NOTE: 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 |
|
202 | Example:
|
203 |
|
204 | ```
|
205 | async function alwaysDeny(req, res) {
|
206 | return false;
|
207 | }
|
208 | ...
|
209 | @AsyncVerify(alwaysDeny)
|
210 | @GET("/unreachable")
|
211 | async someMethod() {
|
212 | ...
|
213 | }
|
214 | ```
|
215 |
|
216 | ### `IsDisabledOn(function)`
|
217 |
|
218 | Add to the decorated method a verification function that will be executed BEFORE the route.
|
219 | The function shall return a boolean value and it is evaluated during the server startup.
|
220 | If 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 |
|
224 | Example:
|
225 |
|
226 | ```
|
227 | function disableOnProduction() {
|
228 | return isProduction == true;
|
229 | }
|
230 | ...
|
231 | @IsDisabledOn(disableOnProduction)
|
232 | @GET("/test")
|
233 | async testMethod() {
|
234 | ...
|
235 | }
|
236 | ```
|
237 |
|
238 | ### `Body(name, schema)`
|
239 |
|
240 | The `Body` decorator inject the request body as a parameter of the decorated method. The body object
|
241 | is automatically wrapped inside a `ValidateObject`, that is verified using a [JOI schema](https://github.com/hapijs/joi).
|
242 | Example:
|
243 |
|
244 | ```
|
245 | import { ValidateObject } from "lynx-framework/validate-object";
|
246 | import * as Joi from "joi";
|
247 |
|
248 | const 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")
|
264 | async 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 |
|
276 | Starting from version `0.5.8`, a new builder class can be used to create Joi schemas.
|
277 | The previous example can be simplified as follows:
|
278 |
|
279 | ```
|
280 | import { ValidateObject, SchemaBuilder } from "lynx-framework/validate-object";
|
281 |
|
282 | ...
|
283 |
|
284 | const 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")
|
295 | async 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 |
|
311 | When an endpoint method is called, the last two arguments always are the original `req` and `res` objects.
|
312 | The `req` object has also the `user` and `files` properties (it is a _Lynx Request Object_).
|
313 | The use of `res` object is discouraged, in favor of a standard returned object from the endpoint method.
|
314 | Example:
|
315 |
|
316 | ```
|
317 | @GET("/endpoint/:id")
|
318 | async myEndpoint(id:Number, req: Request, res: Response) {
|
319 | ...
|
320 | }
|
321 | ```
|
322 |
|
323 | #### `postConstructor`
|
324 |
|
325 | It 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 |
|
331 | The `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 |
|
337 | The `button_login` shall be a property in the JSON localized file.
|
338 |
|
339 | ### `json` filter
|
340 |
|
341 | The `json` filter automatically format an object or variable to JSON.
|
342 |
|
343 | ### `format` filter
|
344 |
|
345 | The `format` filter format a number to a string, with a fixed number of decimal digits (default: 2).
|
346 | Usage:
|
347 |
|
348 | ```
|
349 | <span class="price">€ {{ price | format }}</span>
|
350 | <span class="integer_number">{{ myNumber | format(0) }}
|
351 | ```
|
352 |
|
353 | ### `date` filter
|
354 |
|
355 | The `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.
|
357 | Usage:
|
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 |
|
366 | The `route` function compile a route name to an url with the given parameters.
|
367 | If an url is used instead of a route name, the url is still compiled with the given parameters.
|
368 | Usage:
|
369 |
|
370 | ```
|
371 | <a href="{{route('forgot_password')}}" class="btn btn-link px-0">...</a>
|
372 | ```
|
373 |
|
374 | To set the name of a route, use the `Name` decorated to the chosen method.
|
375 |
|
376 | ### `old` global function
|
377 |
|
378 | The `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.
|
379 | Usage:
|
380 |
|
381 | ```
|
382 | <input type="email" name="email" class="form-control" value="{{old('email')}}">
|
383 | ```
|
384 |
|
385 | ### `currentHost` global function
|
386 |
|
387 | The `currentHost` function is used to retrieve the current server host. This can be used, with the `route` function, to generate an absolute
|
388 | url (for example, needed to generate an url for an email).
|
389 |
|
390 | ### Add custom filters and functions to the template engine.
|
391 |
|
392 | It is possible to add new custom filters and functions using the `TemplateFilter` and `TemplateFunction` decorators.
|
393 | In both cases, it is necessary to create a new class for each filter or function, inside the `templating` folder of the module.
|
394 |
|
395 | NOTE: 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 |
|
399 | Example: create a new `currency` filter.
|
400 | Create a new `currency.filter.ts` file inside the `templating` folder as follows:
|
401 |
|
402 | ```
|
403 | import { TemplateFilter } from 'lynx-framework/templating/decorators';
|
404 | import BaseFilter from 'lynx-framework/templating/base.filter';
|
405 |
|
406 | @TemplateFilter('currency')
|
407 | export 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 |
|
421 | Example: create a new `placeholderUrl` function.
|
422 | Create a new `placeholder-url.function.ts` file inside the `templating` folder as follows:
|
423 |
|
424 | ```
|
425 | import { TemplateFunction } from 'lynx-framework/templating/decorators';
|
426 | import BaseFunction from 'lynx-framework/templating/base.function';
|
427 |
|
428 | @TemplateFunction('placeholderUrl')
|
429 | export 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 |
|
449 | Starting from `1.0.0-rc4`, it is possible to customize the standard response of the `API` tagged routes.
|
450 |
|
451 | To achieve this feature, it is necessary to implement the `APIResponseWrapper` interface, and set the `apiResponseWrapper` property of your `App` instance.
|
452 | By default, the `DefaultResponseWrapper` implementation is used.
|
453 |
|
454 | ## Lynx Modules
|
455 |
|
456 | Lynx 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.
|
457 | Modules shall be loaded at startup time, and shall be injected in the Lynx application constructor:
|
458 |
|
459 | ```
|
460 | const app = new App(myConfig, [new DatagridModule(), new AdminUIModule()] as BaseModule[]);
|
461 | ```
|
462 |
|
463 | In this example, the Lynx application is created with the `DatagridModule` and the `AdminUIModule` modules.
|
464 |
|
465 | Modules are the standard to provide additional functionality to the Lynx framework.
|
466 |
|
467 | ## Mail Client
|
468 |
|
469 | Since 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 |
|
471 | The `App` class define the `mailClient` property of type `MailClient`. This class contains the methods `sendMail` and `sendMailRaw` to respectivly send emails.
|
472 | The 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 |
|
474 | The mail client is configured thought the `ConfigBuilder` of the application.
|
475 |
|
476 | By 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 |
|
482 | Lynx 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 |
|
486 | This interceptor can be mounded as an additional `express Router`, and it is executed before any middleware or routes defined by the lynx modules system.
|
487 | Seems 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 |
|
493 | This interceptor is executed just before the `performResponse` method of any Lynx `Response` is executed.
|
494 | This 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 |
|
498 | At Jellyfish Solutions, we use this two interceptors in conjunction, in order to manage url rewrite based on the language.
|
499 | In 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.
|
500 | The _Global Routing Interceptor_ is used to remove the language from the request url, and correctly update the usual request language.
|
501 | The _Perform Response Interceptor_ is used to add (if necessary) the correct language to a redirect response, without editing the application code.
|