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 | - **[GraphQL](http://graphql.org/)** to automatically exposes an API to the database entities;
|
17 | - **[JWT](https://jwt.io/)** to enable token authentication;
|
18 | - **[multer](https://github.com/expressjs/multer)** to manage file upload;
|
19 | - **[nodemailer](https://nodemailer.com)** to send emails;
|
20 | - **[joi](https://github.com/hapijs/joi)** to validate the requests;
|
21 | - **[jimp](https://github.com/oliver-moran/jimp)** to perform image resizing and other operations.
|
22 |
|
23 | ## Out-Of-The-Box Features
|
24 |
|
25 | With Lynx, you will have the following functionality out of the box:
|
26 |
|
27 | - user management, and low level function for login, registration, authorization and authentication;
|
28 | - media upload management, file upload and retrieving, on a virtual-folder environment;
|
29 | - 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!
|
30 | - GraphQL (queries and mutations!) automatically generated based on your Database entities and decoration.
|
31 | - datatables, directly integrated on the template engine, with pagination, filtering and ordering.
|
32 |
|
33 | ## Installation
|
34 |
|
35 | ```
|
36 | npm install lynx-framework
|
37 | ```
|
38 |
|
39 | ## Lynx application structure
|
40 |
|
41 | A Lynx application shall be formed by different folders:
|
42 |
|
43 | ```
|
44 | .
|
45 | ├── controllers
|
46 | │ ├── backoffice
|
47 | │ │ └── main.controller.ts
|
48 | │ └── main.controller.ts
|
49 | ├── entities
|
50 | ├── index.ts
|
51 | ├── libs
|
52 | ├── locale
|
53 | │ ├── en.json
|
54 | │ └── it.json
|
55 | ├── middlewares
|
56 | │ └── always.middleware.ts
|
57 | ├── public
|
58 | └── views
|
59 | └── main.njk
|
60 | ```
|
61 |
|
62 | - The `controllers` folder shall contain (with subfolder support) any controllers.
|
63 | - The `entities` folder shall contain any entities, that will be automatically mapped with `TypeORM`.
|
64 | - The `libs` folder shall contain any additional libraries or utility functions, that can be used by controllers and middlewares.
|
65 | - The `local` folder shall contains localization file formatted as key-value JSON file.
|
66 | - The `middlewares` folder shall contain (with subfolder support) any middleware.
|
67 | - The `public` folder shall contain all the public resources, such as images, css and so on.
|
68 | - The `view` folder shall contain the nunjucks templates. An `emails` subfolder, containing the email templates, is recommended.
|
69 |
|
70 | The project structure can be customized.
|
71 |
|
72 | ## Lynx application
|
73 |
|
74 | To start a Lynx application, it is necessary to instantiate a Lynx `App` object. For example, the `index.ts` file can be:
|
75 |
|
76 | ```
|
77 | import { App, ConfigBuilder } from "lynx-framework/app";
|
78 |
|
79 | const port = Number(process.env.PORT) || 3000;
|
80 |
|
81 | const app = new App(new ConfigBuilder(__dirname).build());
|
82 | app.startServer(port);
|
83 | ```
|
84 |
|
85 | Any Lynx configuration, such as database connection, token secrets, folders and so on, can be customized using the `ConfigBuilder` object.
|
86 | Any controllers, middlewares and entities will be automatically loaded by the Lynx app.
|
87 |
|
88 | ## Controllers
|
89 |
|
90 | A controller defines a set of endpoints. Any endpoint responds to a specific
|
91 | path and HTTP verb, and can generate an HTML or JSON response.
|
92 | Any controller shall extends the `BaseController` class, in order to inherit a lot of
|
93 | utility methods.
|
94 | It is possible to define only ONE controller for each file, and the class shall be `export default`.
|
95 | Moreover, the file should be named as `controllerName.controller.ts`.
|
96 |
|
97 | The minimum configuration of a controller is the following:
|
98 |
|
99 | ```
|
100 | import { Route } from "lynx-framework/decorators";
|
101 | import BaseController from "lynx-framework/base.controller";
|
102 |
|
103 | @Route("/myController/path")
|
104 | export default class MyController extends BaseController {
|
105 | ...
|
106 | }
|
107 | ```
|
108 |
|
109 | To define endpoints, it is necessary to decor a method. There is a decorator for each HTTP verb (GET, POST, etc..). For example:
|
110 |
|
111 | ```
|
112 | ...
|
113 | @GET('/helloWorld')
|
114 | async helloWorld() {
|
115 | return "Hello, world!";
|
116 | }
|
117 | ```
|
118 |
|
119 | the method `helloWorld` will be executed for any `GET` request to `/myController/path/helloWorld`.
|
120 |
|
121 | ### Method decorators
|
122 |
|
123 | #### `GET(path)`, `POST(path)`, `PUT(path)`, `PATCH(path)`, `DELETE(path)`
|
124 |
|
125 | These decorators map the method to the chosen HTTP verb with the specified path.
|
126 | Moreover, `path` can contains path parameters, for example `/authors/:id/posts`. The path parameters will be injected in the method as arguments:
|
127 |
|
128 | ```
|
129 | @GET('/authors/:id/posts/:secondParameter')
|
130 | async doubleParameters(id:Number, secondParameter:String) {
|
131 | ...
|
132 | }
|
133 | ```
|
134 |
|
135 | Since Lynx is based on Express, more information about the url parameters can be found [here](https://expressjs.com/en/guide/routing.html).
|
136 |
|
137 | **IMPORTANT** these decorators shall be put just before the method definition, and as last decorator used to the method.
|
138 |
|
139 | ### `API()`
|
140 |
|
141 | The `API` decorator enforce the serialization of the returned object to JSON. This feature is very useful to build an API.
|
142 | In this case, the returned object will be added inside the following standard return object:
|
143 |
|
144 | ```
|
145 | {
|
146 | "success": true/false,
|
147 | "data": "returned object serialized"
|
148 | }
|
149 | ```
|
150 |
|
151 | If the method returns a boolean value, the return object will be:
|
152 |
|
153 | ```
|
154 | {
|
155 | "success": returned value
|
156 | }
|
157 | ```
|
158 |
|
159 | For more information about the object serialization, please check the [Entity chapter]().
|
160 |
|
161 | ### `MultipartForm()`
|
162 |
|
163 | This decorator simply allows the MultipartForm in post request. It is essential to enable the automatic file upload system.
|
164 |
|
165 | ### `Name(name)`
|
166 |
|
167 | This decorator allows to set a name to the route. So, it is possible to recall this route in a very simple way.
|
168 | The route name is used by any `route` functions.
|
169 |
|
170 | ### `Verify(function)`
|
171 |
|
172 | Add to the decorated method a verification function that will be executed BEFORE the route.
|
173 | 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.
|
174 | NOTE: the function shall NOT be a class method, but a proper Typescript function.
|
175 | Example:
|
176 |
|
177 | ```
|
178 | function alwaysDeny(req, res) {
|
179 | return false;
|
180 | }
|
181 | ...
|
182 | @Verify(alwaysDeny)
|
183 | @GET("/unreachable")
|
184 | async someMethod() {
|
185 | ...
|
186 | }
|
187 | ```
|
188 |
|
189 | ### `AsyncVerify(function)`
|
190 |
|
191 | Add to the decorated method a verification function that will be executed BEFORE the route.
|
192 | 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.
|
193 | NOTE: the function shall NOT be a class method, but a proper Typescript function.
|
194 |
|
195 | > This method is available from version 0.5.5
|
196 |
|
197 | Example:
|
198 |
|
199 | ```
|
200 | async function alwaysDeny(req, res) {
|
201 | return false;
|
202 | }
|
203 | ...
|
204 | @AsyncVerify(alwaysDeny)
|
205 | @GET("/unreachable")
|
206 | async someMethod() {
|
207 | ...
|
208 | }
|
209 | ```
|
210 |
|
211 | ### `Body(name, schema)`
|
212 |
|
213 | The `Body` decorator inject the request body as a parameter of the decorated method. The body object
|
214 | is automatically wrapped inside a `ValidateObject`, that is verified using a [JOI schema](https://github.com/hapijs/joi).
|
215 | Example:
|
216 |
|
217 | ```
|
218 | import { ValidateObject } from "lynx-framework/validate-object";
|
219 | import * as Joi from "joi";
|
220 |
|
221 | const loginSchema = Joi.object().keys({
|
222 | email: Joi.string()
|
223 | .email()
|
224 | .required()
|
225 | .label("{{input_email}}"), //I can use a localized string!
|
226 | password: Joi.string()
|
227 | .required()
|
228 | .min(4)
|
229 | .regex(/^[a-zA-Z0-9]{3,30}$/)
|
230 | .label("{{input_password}}") //I can use a localized string!
|
231 | });
|
232 |
|
233 | ...
|
234 |
|
235 | @Body("d", loginSchema)
|
236 | @POST("/login")
|
237 | async performLogin(
|
238 | d: ValidateObject<{ email: string; password: string }>
|
239 | ) {
|
240 | if (!d.isValid) {
|
241 | //d.errors contains localized errors!
|
242 | return false;
|
243 | }
|
244 | let unwrapped = d.obj; //I can use unwrapped.email and unwrapped.password!
|
245 | ...
|
246 | }
|
247 | ```
|
248 |
|
249 | Starting from version `0.5.8`, a new builder class can be used to create Joi schemas.
|
250 | The previous example can be simplified as follows:
|
251 |
|
252 | ```
|
253 | import { ValidateObject, SchemaBuilder } from "lynx-framework/validate-object";
|
254 |
|
255 | ...
|
256 |
|
257 | const loginSchema = new SchemaBuilder()
|
258 | .email("email")
|
259 | .withLabel("{{input_email}}")
|
260 | .password("password")
|
261 | .withLabel("{{input_password}}")
|
262 | .build();
|
263 |
|
264 | ...
|
265 |
|
266 | @Body("d", loginSchema)
|
267 | @POST("/login")
|
268 | async performLogin(
|
269 | d: ValidateObject<{ email: string; password: string }>
|
270 | ) {
|
271 | if (!d.isValid) {
|
272 | //d.errors contains localized errors!
|
273 | return false;
|
274 | }
|
275 | let unwrapped = d.obj; //I can use unwrapped.email and unwrapped.password!
|
276 | ...
|
277 | }
|
278 | ```
|
279 |
|
280 | ### Advanced
|
281 |
|
282 | #### Accessing the original `req` and `res`
|
283 |
|
284 | When an endpoint method is called, the last two arguments always are the original `req` and `res` objects.
|
285 | The `req` object has also the `user` and `files` properties (it is a _Lynx Request Object_).
|
286 | The use of `res` object is discouraged, in favor of a standard returned object from the endpoint method.
|
287 | Example:
|
288 |
|
289 | ```
|
290 | @GET("/endpoint/:id")
|
291 | async myEndpoint(id:Number, req: Request, res: Response) {
|
292 | ...
|
293 | }
|
294 | ```
|
295 |
|
296 | #### `postConstructor`
|
297 |
|
298 | 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.
|
299 |
|
300 | ## Enhancements to the Nunjucks engine
|
301 |
|
302 | ### `tr` filter
|
303 |
|
304 | The `tr` filter automatically localize a string. Usage:
|
305 |
|
306 | ```
|
307 | <button type="submit" class="btn btn-primary px-4">{{ "button_login" | tr }}</button>
|
308 | ```
|
309 |
|
310 | The `button_login` shall be a property in the JSON localized file.
|
311 |
|
312 | ### `json` filter
|
313 |
|
314 | The `json` filter automatically format an object or variable to JSON.
|
315 |
|
316 | ### `format` filter
|
317 |
|
318 | The `format` filter format a number to a string, with a fixed number of decimal digits (default: 2).
|
319 | Usage:
|
320 |
|
321 | ```
|
322 | <span class="price">€ {{ price | format }}</span>
|
323 | <span class="integer_number">{{ myNumber | format(0) }}
|
324 | ```
|
325 |
|
326 | ### `date` filter
|
327 |
|
328 | The `date` filter format a date to a string, using the `moment`. The default format will use the
|
329 | `lll` format, but it is possible to override this behavior.
|
330 | Usage:
|
331 |
|
332 | ```
|
333 | <span class="date">€ {{ data.createdAt | date }}</span>
|
334 | <span class="my_date_custom">{{ data.createdAt | date("YYYY-MM-DD") }}
|
335 | ```
|
336 |
|
337 | ### `route` global function
|
338 |
|
339 | The `route` function compile a route name to an url with the given parameters.
|
340 | If an url is used instead of a route name, the url is still compiled with the given parameters.
|
341 | Usage:
|
342 |
|
343 | ```
|
344 | <a href="{{route('forgot_password')}}" class="btn btn-link px-0">...</a>
|
345 | ```
|
346 |
|
347 | To set the name of a route, use the `Name` decorated to the chosen method.
|
348 |
|
349 | ### `old` global function
|
350 |
|
351 | 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.
|
352 | Usage:
|
353 |
|
354 | ```
|
355 | <input type="email" name="email" class="form-control" value="{{old('email')}}">
|
356 | ```
|
357 |
|
358 | ### `currentHost` global function
|
359 |
|
360 | The `currentHost` function is used to retrieve the current server host. This can be used, with the `route` function, to generate an absolute
|
361 | url (for example, needed to generate an url for an email).
|
362 |
|
363 | ## Custom `API` response
|
364 |
|
365 | Starting from `1.0.0-rc4`, it is possible to customize the standard response of the `API` tagged routes.
|
366 |
|
367 | To achieve this feature, it is necessary to implement the `APIResponseWrapper` interface, and set the `apiResponseWrapper` property of your `App` instance.
|
368 | By default, the `DefaultResponseWrapper` implementation is used. |
\ | No newline at end of file |