loopback4 experience

Recently, I read the doc of loopback4 on a whim. To be honest, I vomited a little blood when I read it. It may be that I am too bad. I don’t say the English one. This is okay. There are no strange words, but they are from east to west. Overall, there are two feelings. First, it seems that typing a few lines of command code will come out, but if you don’t understand those strange syntactic sugar words, you can’t type your own code, which is inflexible. Second, doc does not see A complete example, all of which suddenly give you a github link halfway through, asking you to see the project.

But after watching it for so long, let me briefly summarize how to use it to build a project

Install Scaffolding

1
npm i -g @loopback/cli

If you are using

1
yarn global add @loopback/cli

Be careful to add your yarn bin to the environment variables, otherwise you will not be able to perform the following step

Create a new project

1
lb4 app

Answer the prompts as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
? Project name: getting-started
? Project description: Getting started tutorial
? Project root directory: (getting-started)
? Application class name: StarterApplication
? Select features to enable in the project:
❯◉ Enable eslint: add a linter with pre-configured lint rules
◉ Enable prettier: install prettier to format code conforming to rules
◉ Enable mocha: install mocha to run tests
◉ Enable loopbackBuild: use @loopback/build helpers (e.g. lb-eslint)
◉ Enable vscode: add VSCode config files
◉ Enable docker: include Dockerfile and .dockerignore
◉ Enable repositories: include repository imports and RepositoryMixin
◉ Enable services: include service-proxy imports and ServiceMixin

Then, you can start running your own project.

Creating an Empty Controller

1
lb4 controller
  • Note: If your application is still running, press CTRL+C to stop it before calling the command

  • Answer the prompts as follows:

    1
    2
    3
    4
    5
    6
    ? Controller class name: hello
    ? What kind of controller would you like to generate? Empty Controller
    create src/controllers/hello.controller.ts
    update src/controllers/index.ts

    Controller hello was now created in src/controllers/
  • Paste the following contents into the file /src/controllers/hello.controller.ts:

    1
    2
    3
    4
    5
    6
    7
    8
    import {get} from '@loopback/rest';

    export class HelloController {
    @get('/hello')
    hello(): string {
    return 'Hello world!';
    }
    }
  • Start the application using npm start.

  • Visit http://127.0.0.1:3000/hello to see Hello world!

Everything looks good together so far.

Creating a CRUD

To create a Controller with a CRUD project, first you need to have a model, or borrow the official doc, and these steps cannot be messed up

Create a model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
lb4 model
? Model class name: todo
? Please select the model base class Entity (A persisted model with an ID)
? Allow additional (free-form) properties? No
Model Todo will be created in src/models/todo.model.ts

Let's add a property to Todo
Enter an empty property name when done

? Enter the property name: id
? Property type: number
? Is id the ID property? Yes
? Is id generated automatically? No
? Is it required?: No
? Default value [leave blank for none]:

Let's add another property to Todo
Enter an empty property name when done

? Enter the property name: title
? Property type: string
? Is it required?: Yes
? Default value [leave blank for none]:

Let's add another property to Todo
Enter an empty property name when done

? Enter the property name: desc
? Property type: string
? Is it required?: No
? Default value [leave blank for none]:

Let's add another property to Todo
Enter an empty property name when done

? Enter the property name: isComplete
? Property type: boolean
? Is it required?: No
? Default value [leave blank for none]:

Let's add another property to Todo
Enter an empty property name when done

? Enter the property name:

create src/models/todo.model.ts
update src/models/index.ts

Model Todo was created in src/models/

Creating a DataSource

1
2
3
4
5
6
7
8
9
10
lb4 datasource
? Datasource name: db
? Select the connector for db: In-memory db (supported by StrongLoop)
? window.localStorage key to use for persistence (browser only):
? Full path to file for persistence (server only): ./data/db.json

create src/datasources/db.datasource.ts
update src/datasources/index.ts

Datasource Db was created in src/datasources/

创建Repository

1
2
3
4
5
6
7
8
9
lb4 repository
? Please select the datasource DbDatasource
? Select the model(s) you want to generate a repository Todo
? Please select the repository base class DefaultCrudRepository (Juggler bridge)

create src/repositories/todo.repository.ts
update src/repositories/index.ts

Repository TodoRepository was created in src/repositories/

Creating a Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lb4 controller
? Controller class name: todo
Controller Todo will be created in src/controllers/todo.controller.ts

? What kind of controller would you like to generate? REST Controller with CRUD functions
? What is the name of the model to use with this CRUD repository? Todo
? What is the name of your CRUD repository? TodoRepository
? What is the name of ID property? id
? What is the type of your ID? number
? Is the id omitted when creating a new instance? Yes
? What is the base HTTP path name of the CRUD operations? /todos
create src/controllers/todo.controller.ts
update src/controllers/index.ts

Controller Todo was created in src/controllers/

Create Relations

Through the above steps, we create a TodoList model and repositorie.

Then we create a connection between the two of them

1
2
3
4
5
6
7
8
9
10
$ lb4 relation
? Please select the relation type hasMany
? Please select source model TodoList
? Please select target model Todo
? Foreign key name to define on the target model todoListId
? Source property name for the relation getter (will be the relation name) todos
? Allow TodoList queries to include data from related Todo instances? Yes
create src/controllers/todo-list-todo.controller.ts

Relation HasMany was created in src/

This creates a connection between the two models.

Note that both must have a repositorie, otherwise the association cannot be created.

Because the Create Association step will create a Controller.

Add JWT validation

1
2
3
$ lb4 example todo
$ cd loopback4-example-todo
$ npm i --save @loopback/authentication @loopback/authentication-jwt

Binding Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
src/application.ts

// ---------- ADD IMPORTS -------------
import {AuthenticationComponent} from '@loopback/authentication';
import {
JWTAuthenticationComponent,
SECURITY_SCHEME_SPEC,
UserServiceBindings,
} from '@loopback/authentication-jwt';
import {DbDataSource} from './datasources';
// ------------------------------------

export class TodoListApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
constructor(options: ApplicationConfig = {}) {
//...
// ------ ADD SNIPPET AT THE BOTTOM ---------
// Mount authentication system
this.component(AuthenticationComponent);
// Mount jwt component
this.component(JWTAuthenticationComponent);
// Bind datasource
this.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME);
// ------------- END OF SNIPPET -------------
}
}

Create UserController

1
2
3
4
5
6
7
8
9
10
11
$ lb4 controller
? Controller class name: User
Controller User will be created in src/controllers/user.controller.ts

? What kind of controller would you like to generate? Empty Controller

create src/controllers/user.controller.ts
update src/controllers/index.ts

Controller User was created in src/controllers/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/src/controllers/user.controller.ts

// ---------- ADD IMPORTS -------------
import {inject} from '@loopback/core';
import {
TokenServiceBindings,
MyUserService,
UserServiceBindings,
UserRepository,
} from '@loopback/authentication-jwt';
import {TokenService} from '@loopback/authentication';
import {SecurityBindings, UserProfile} from '@loopback/security';
import {repository} from '@loopback/repository';
// ----------------------------------

constructor(
@inject(TokenServiceBindings.TOKEN_SERVICE)
public jwtService: TokenService,
@inject(UserServiceBindings.USER_SERVICE)
public userService: MyUserService,
@inject(SecurityBindings.USER, {optional: true})
public user: UserProfile,
@repository(UserRepository) protected userRepository: UserRepository,
) {}

Add JWT validation

1
2
3
4
5
6
7
// ---------- ADD IMPORTS -------------
import {authenticate} from '@loopback/authentication';
// ------------------------------------
@authenticate('jwt') // <---- Apply the @authenticate decorator at the class level
export class TodoController {
//...
}

Question

So far, you can do todo CRUD through the api provided by loopback, it looks happy, doesn’t it?

But there are a few questions I didn’t find the answers to in the doc

  • I can’t find a place where I can update two models with one request, such as TodoList and Todo above. I want to update these two related models, I have to make two requests.

  • Customizing the Request body is very cumbersome, and you must strictly follow its specifications.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const requestBodyObject = {
    description: 'data',
    content: {
    'application/x-www-form-urlencoded': {
    schema: {
    type: 'object',
    properties: {
    name: {type: 'string'},
    location: {
    type: 'object',
    properties: {
    lat: {type: 'number'},
    lng: {type: 'number'},
    },
    },
    tags: {
    type: 'array',
    items: {type: 'string'},
    },
    },
    },
    },
    },
    };
  • The type of a variable I defined in Model is an enum type I defined, and the project cannot be started. The reason for the error is that the type I defined cannot be resolved.

  • With its JWT verification, you can not customize the UserModel, although it is convenient, but I did not find a place where I can customize the User, so I can not add the field I need to the User.

  • I used a package called casbin for authentication, and the doc is also all English, so I don’t quite understand it.

These are some of the problems I have encountered, and I hope that as I deepen my understanding of it, I can solve it.

Update on 2021.03.20 (fill in the problems in the previous issue)

How to Customize RequestBody

At present, the fastest way is to create a Model through’lb4 model ', the format is the RequestBody you need, of course, you can also completely type one yourself, for example:

1
2
3
4
5
6
7
8
@model()
export class NewUserRequest extends User {
@property({
type: 'string',
required: true,
})
password: string;
}

Then on the request

1
2
3
4
5
6
7
8
9
@requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(NewUserRequest, {
title: 'NewUser',
}),
},
},
})

How to use your own model for JWT verification

Change the binding of User, UserCredentials, RefreshToken, Model, Repository, Service in application.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//application.ts


// Mount authentication system
this.component(AuthenticationComponent);
// Mount jwt component
this.component(JWTAuthenticationComponent);
// Bind datasource
this.dataSource(MemoryDataSource, UserServiceBindings.DATASOURCE_NAME);

this.bind(UserServiceBindings.USER_REPOSITORY).toClass(UserRepository);
this.bind(UserServiceBindings.USER_CREDENTIALS_REPOSITORY).toClass(
UserCredentialsRepository,
);
this.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService);


this.bind(TokenServiceBindings.TOKEN_SECRET).to('my-secret');
this.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(JWTService);

this.bind(RefreshTokenServiceBindings.REFRESH_REPOSITORY).toClass(RefreshTokenRepository);
this.bind(RefreshTokenServiceBindings.REFRESH_TOKEN_SERVICE).toClass(RefreshtokenService);
this.bind(RefreshTokenServiceBindings.REFRESH_SECRET).to(
'my-refresh-secret',
);

this.bind(TokenServiceBindings.TOKEN_EXPIRES_IN).to((60 * 60).toString());
this.bind(RefreshTokenServiceBindings.REFRESH_EXPIRES_IN).to(
(7 * 24 * 60 * 60).toString(),
);