@loopback/docs
Version:
Documentation files rendered at [https://loopback.io](https://loopback.io)
502 lines (399 loc) • 14.7 kB
Markdown
---
lang: en
title: 'Migrating custom model methods'
keywords:
LoopBack 4.0, LoopBack 4, Node.js, TypeScript, OpenAPI, LoopBack 3, Migration
sidebar: lb4_sidebar
permalink: /doc/en/lb4/migration-models-methods.html
---
{% include tip.html content="
Missing instructions for your LoopBack 3 use case? Please report a [Migration docs issue](https://github.com/loopbackio/loopback-next/issues/new?labels=question,Migration,Docs&template=Migration_docs.md) on GitHub to let us know.
" %}
## Introduction
This document will guide you in migrating custom model methods in LoopBack 3 to
their equivalent implementations in LoopBack 4.
In LoopBack 3, a model class has three responsibilities: a model describing
shape of data, a repository providing data-access APIs, and a controller
implementing REST API.
In LoopBack 4,
- data-access APIs are implemented by [repositories](../../Repository.md) that
are decoupled from models.
- REST APIs are implemented by [controllers](docs/site/Controller.md) that are
decoupled from models.
A `Repository` represents a specialized service interface that provides
strong-typed data access (for example, CRUD) operations of a domain model
against the underlying database or service. A single model can be used with
multiple different repositories. A Repository can be defined and implemented by
application developers. LoopBack ships a few predefined repository interfaces
for typical CRUD and KV operations. These repository implementations leverage
model definition and dataSource configuration to fulfill the logic for data
access. See more examples at
[Repository/CrudRepository/EntityRepository](https://github.com/loopbackio/loopback-next/blob/master/packages/repository/src/repositories/repository.ts)
and
[KeyValueRepository](https://github.com/loopbackio/loopback-next/blob/master/packages/repository/src/repositories/kv.repository.ts).
A `Controller` is a class that implements operations defined by an application’s
API. It implements an application’s business logic and acts as a bridge between
the HTTP/REST API and domain/database models. A Controller operates only on
processed input and abstractions of backend services / databases.
Before we discuss any customizations and how to migrate them, let's first go
over some LoopBack 4 basics with regards to models, repositories, and
controllers.
A CLI-generated model named `Note` would look like this:
{% include code-caption.html content="src/models/note.model.ts" %}
```ts
()
export class Note extends Entity {
({
type: 'number',
id: true,
generated: true,
})
id?: number;
({
type: 'string',
required: true,
})
title: string;
({
type: 'string',
})
content?: string;
constructor(data?: Partial<Note>) {
super(data);
}
}
export interface NoteRelations {
// describe navigational properties here
}
export type NoteWithRelations = Note & NoteRelations;
```
A [model](docs/site/Model.md) describes business domain objects, for example,
Customer, Address, and Order. It usually defines a list of properties with name,
type, and other constraints like [relations](docs/site/Relations.md).
A CLI-generated **repository** for a model `Note` would look like this:
{% include code-caption.html content="src/repositories/note.repository.ts" %}
```ts
export class NoteRepository extends DefaultCrudRepository<
Note,
typeof Note.prototype.id,
NoteRelations
> {
constructor(('datasources.db') dataSource: DbDataSource) {
super(Note, dataSource);
}
}
```
It extends
[DefaultCrudRepository](https://github.com/loopbackio/loopback-next/blob/master/packages/repository/src/repositories/legacy-juggler-bridge.ts)
. It handles all persistence operations against the datasource.
A CLI-generated **controller** for the model `Note` would look like this: (To
save space, only a **partial** implementation is shown)
{% include code-caption.html content="src/controllers/note.controller.ts" %}
```ts
export class NoteController {
constructor(
(NoteRepository)
public noteRepository: NoteRepository,
) {}
('/notes', {
responses: {
'200': {
description: 'Note model instance',
content: {'application/json': {schema: getModelSchemaRef(Note)}},
},
},
})
async create(
({
content: {
'application/json': {
schema: getModelSchemaRef(Note, {
title: 'NewNote',
exclude: ['id'],
}),
},
},
})
note: Omit<Note, 'id'>,
): Promise<Note> {
return this.noteRepository.create(note);
}
// ...
// remaining CRUD endpoints
// ...
}
```
For a full example of a CLI-generated controller for a model `Todo`, see
[TodoController ](https://github.com/loopbackio/loopback-next/blob/master/examples/todo/src/controllers/todo.controller.ts).
One of the first things you will notice in the controller is that it injects the
repository `NoteRepository` to handle all persistence operations.
Looking at `NoteController`'s `create` method, you will notice that it is
decorated with a `` decorator to indicate that it handles POST requests. It
also defines the endpoint url `'/notes'` and its OpenAPI operation
specification. The controller's `create` method relies on the `NoteRepository`'s
`create` method to handle the actual persistence operation.
The remaining controller methods are set up in a similar way.
## LoopBack 3 Approaches to Customizing Model Methods
In LoopBack 3, a developer is able to customize model methods in various ways:
- [configure which endpoints are public](#configure-which-endpoints-are-public)
- [customize model method, but not the endpoint](#customize-model-method-but-not-the-endpoint)
- [add a new model method and a new endpoint](#add-a-new-model-method-and-a-new-endpoint)
In the sections below, we will show some examples of these, and how to implement
them in LoopBack 4.
## Configure Which Endpoints are public
By default, LoopBack 3 and 4 automatically makes each endpoint public. Steps
must be taken in LoopBack 3 and 4 to prevent an endpoint from being public.
### LoopBack 3 Approach
In LoopBack 3 LoopBack models automatically have a standard set of HTTP
endpoints that provide REST APIs for create, read, update, and delete (CRUD)
operations on model data.
To control whether the REST API from is public or not, the developer has two
options:
- specifying certain options in `server/model-config.json`
- calling `disableRemoteMethodByName()` from model's script file
See
[Exposing and hiding models, methods, and endpoints](https://loopback.io/doc/en/lb3/Exposing-models-over-REST.html#exposing-and-hiding-models-methods-and-endpoints)
for details.
{% include code-caption.html content="/server/model-config.json" %}
```json
"Note": {
"public": true,
"dataSource": "db"
},
```
This exposes all the REST API endpoints of the model. The default for `public`
is `true`, so it is not necessary to add this property at all. But doing so
makes it very clear. To **hide** all the REST API endpoints of the model, simply
change `public` to `false`.
{% include code-caption.html content="/server/model-config.json" %}
```json
"MyUser": {
"public": true,
"dataSource": "db",
"options": {
"remoting": {
"sharedMethods": {
"*": false,
"login": true,
"logout": true
}
}
}
}
```
This hides all the REST API endpoints of the model, except `login and logout`.
{% include code-caption.html content="common/models/Note.js" %}
```js
Note.disableRemoteMethodByName('create');
Note.disableRemoteMethodByName('upsert');
Note.disableRemoteMethodByName('deleteById');
...
```
This disables specific REST API endpoints of the model by name.
### LoopBack 4 Approach
As mentioned earlier, REST API endpoints are implemented by a controller class.
Preventing an endpoint from being public is simple.
To **hide** a particular REST API endpoint, simply **delete** the appropriate
function and its decorator from the controller class.
To **hide all** REST API endpoints of a particular model, simply **do not
create** a controller class for it.
## Customize Model Method But Not the Endpoint
LoopBack 3 and 4 provide a lot of out-of-the-box functionality for developers
with regards to data-access APIs and REST APIs. Only a few steps are required to
create minor customizations.
### LoopBack 3 Approach
To override the behaviour of a
[PersistedModel](https://apidocs.loopback.io/loopback/#persistedmodel) the
developer has two options for writing **custom methods**:
- [Via server boot script](https://loopback.io/doc/en/lb3/Customizing-models.html#via-server-boot-script)
- [Via model's script](https://loopback.io/doc/en/lb3/Customizing-models.html#via-your-models-script)
The two approaches above are similar, so, for brevity, we will only discuss the
`model script` approach in the examples below.
The following LB3 code snippet shows how to customize the built-in
[find()](https://apidocs.loopback.io/loopback/#persistedmodel-find) method for
the model `Note`.
{% include code-caption.html content="common/models/Note.js" %}
```js
module.exports = function (Note) {
Note.on('dataSourceAttached', function (obj) {
var find = Note.find;
var cache = {};
Note.find = function (filter, options, cb) {
var key = '';
if (filter) {
key = JSON.stringify(filter);
}
var cachedResults = cache[key];
if (cachedResults) {
console.log('serving from cache');
process.nextTick(function () {
cb(null, cachedResults);
});
} else {
console.log('serving from db');
find.call(Note, function (err, results) {
if (!err) {
cache[key] = results;
}
cb(err, results);
});
}
};
});
};
```
It basically builds up a cache to limit database queries. No changes are made to
any REST API endpoint settings.
### LoopBack 4 Approach
As mentioned earlier, in LoopBack 4, data-access APIs are implemented by
repositories. The repository for your model usually extends a default
implementation. So to customize the behaviour of a particular method, you simply
override this method in your repository class.
The following LB4 code snippets show how to customize the **find** method of
`NoteRepository`.
{% include code-caption.html content="src/repositories/note.repository.ts" %}
```ts
export class NoteRepository extends DefaultCrudRepository<
Note,
typeof Note.prototype.id,
NoteRelations
> {
constructor(
('datasources.db') dataSource: DbDataSource,
('my.cache') private cache: Map<string, Note[]>,
) {
super(Note, dataSource);
}
async find(
filter?: Filter<Note>,
options?: Options,
): Promise<(Note & NoteRelations)[]> {
let key: string = '';
if (filter) {
key = JSON.stringify(filter);
}
let cachedResults: Note[] | undefined = this.cache.get(key);
if (cachedResults) {
console.log('serving from cache');
return cachedResults;
} else {
console.log('serving from db');
let results: Note[] | undefined = await super.find(filter, options);
this.cache.set(key, results);
return results;
}
}
}
```
The cache is injected into the repository.
It was defined in the application class:
{% include code-caption.html content="src/application.ts" %}
```ts
export class NoteApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
private cache: Map<string, Note[]> = new Map();
constructor(options: ApplicationConfig = {}) {
super(options);
this.bind('my.cache').to(this.cache);
// ...
}
}
```
No changes are made to any REST API endpoint settings in `NoteController`.
## Add a New Model Method And a New Endpoint
LoopBack 3 and 4 provide a lot of out-of-the-box functionality for developers
with regards to data-access APIs and REST APIs. Only a few steps are required to
create minor additions.
### LoopBack 3 Approach
The following LB3 code snippet shows how to customize the `Note` model by adding
a **new** persisted model method `findByTitle`, and defining a **new** remote
method for its respective endpoint `/findByTitle`. The full endpoint url will
end up being `/Notes/findByTitle`.
{% include code-caption.html content="common/models/Note.js" %}
```js
module.exports = function (Note) {
Note.remoteMethod('findByTitle', {
http: {
path: '/findByTitle',
verb: 'get',
},
accepts: {arg: 'title', type: 'string'},
returns: {arg: 'note', type: [Note], root: true},
});
Note.findByTitle = function (title, cb) {
var titleFilter = {
where: {
title: title,
},
};
Note.find(titleFilter, function (err, foundNotes) {
if (err) {
cb(err);
} else {
cb(null, foundNotes);
}
});
};
};
```
### LoopBack 4 Approach
To accomplish the same thing in LoopBack 4, we can add a **new** method
`findByTitle` to `NoteRepository`, a **new** method `findByTitle` to
`NoteController`, and specify an endpoint url of `/notes/byTitle/{title}`.
{% include code-caption.html content="src/repositories/note.repository.ts" %}
```ts
export class NoteRepository extends DefaultCrudRepository<
Note,
typeof Note.prototype.id,
NoteRelations
> {
constructor(('datasources.db') dataSource: DbDataSource) {
super(Note, dataSource);
}
async findByTitle(title: string): Promise<Note[]> {
const titleFilter = {
where: {
title: title,
},
};
const foundNotes = await this.find(titleFilter);
return foundNotes;
}
}
```
{% include code-caption.html content="src/controllers/note.controller.ts" %}
```ts
export class NoteController {
constructor(
(NoteRepository)
public noteRepository: NoteRepository,
) {}
('/notes/byTitle/{title}', {
responses: {
'200': {
description: 'Array of Note model instances',
content: {
'application/json': {
schema: {
type: 'array',
items: getModelSchemaRef(Note, {includeRelations: true}),
},
},
},
},
},
})
async findByTitle(
.path.string('title') title: string,
): Promise<Note[]> {
return this.noteRepository.findByTitle(title);
}
// ...
// remaining CRUD endpoints
// ...
}
```
## Summary
As the examples above show, migrating custom model methods from LoopBack 3 to
LoopBack 4 is relatively straightforward.