@loopback/docs
Version:
Documentation files rendered at [https://loopback.io](https://loopback.io)
743 lines (597 loc) • 20.9 kB
Markdown
---
lang: en
title: 'Migrating model mixins'
keywords:
LoopBack 4.0, LoopBack 4, Node.js, TypeScript, OpenAPI, LoopBack 3, Migration
sidebar: lb4_sidebar
permalink: /doc/en/lb4/migration-models-mixins.html
---
{% include tip.html content="
Missing instructions for your LoopBack 3 use case? Please report a [Migration docs issue](https://github.com/strongloop/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 mixins, and custom
method/remote method mixins in LoopBack 3 to their equivalent implementations in
LoopBack 4.
For an understanding of how models in LoopBack 3 are now architecturally
decoupled into 3 classes (model, repository, and controller) please read
[Migrating custom model methods](./methods.md).
In LoopBack 3, it was easy to add property mixins and method
[mixins](https://loopback.io/doc/en/lb3/Defining-mixins.html).
In LoopBack 4, it is also easy and is accomplished by using a mixin class
factory function.
## Creating a Property Mixin
This section covers the approach that LoopBack 3 and LoopBack 4 can use to add a
property to a model via a mixin.
### LoopBack 3 Approach
In LoopBack 3, a developer is able to create a model property mixin by:
- placing the mixin logic in a file in a mixins directory
- updating the server/model-config.json file with the mixin directory location
- updating the model's json file to include the mixin's name and a boolean
As an example, we will create a mixin that adds a **category** property to a
model.
#### Defining The Model Property Mixin category.js
The developer defines a model property mixin in **common/mixins/category.js**
which adds a required property named `category` to any model.
{% include code-caption.html content="common/mixins/category.js" %}
```js
module.exports = function (Model, options) {
Model.defineProperty('category', {type: 'string', required: true});
};
```
#### Updating model-config.json
The **server/model-config.json** needs to contain:
- the locations of all **models**
- the location of all **mixins**
- the entry of the model that receives the mixin content (for this example
`Note`)
{% include code-caption.html content="server/model-config.json" %}
```
{
"_meta": {
"sources": [
"loopback/common/models",
"loopback/server/models",
"../common/models",
"./models"
],
"mixins": [
"loopback/common/mixins",
"loopback/server/mixins",
"../common/mixins",
"./mixins"
]
},
// ... other entries
"Note": {
"dataSource": "db"
}
}
```
Please see
[Reference mixins in model-config.js](https://loopback.io/doc/en/lb3/Defining-mixins.html#reference-mixins-in-model-configjs)
for a short explanation of this file.
#### Applying The category.js Mixin To A Model
To extend the model `Note` with the **category.js** mixin, we need to add a
**mixins** section in **common/models/note.json** to indicate which mixins
should be applied to it.
{% include code-caption.html content="common/models/note.json" %}
```json
{
"name": "Note",
"properties": {
"title": {
"type": "string",
"required": true
},
"content": {
"type": "string"
}
},
"mixins": {
"Category": true
}
}
```
Specifying a value of **true** for `Category` will apply the **category.js**
property model mixin to the `Note` model. A value of **false** will not apply
the mixin.
### LoopBack 4 Approach
In LoopBack 4, a developer is able to create a model property mixin by:
- creating a base model class which extends `Entity`
- placing the mixin class factory function in a separate file
- generating a model using the CLI as usual
- adjusting the model file to make use of the mixin class factory function
#### Defining A BaseEntity Class Which Extends Entity
Let's define a base model class `BaseEntity` in **src/models/base-entity.ts**.
It will be used as input to the mixin later.
{% include code-caption.html content="src/models/base-entity.ts" %}
```ts
import {Entity} from '/repository';
export class BaseEntity extends Entity {}
```
This is necessary because the
[Entity](https://github.com/strongloop/loopback-next/blob/master/packages/repository/src/model.ts#L276)
class is abstract and doesn't have a constructor.
#### Defining The Model Property Mixin Class Factory Function
This mixin class factory function `AddCategoryPropertyMixin` in
**src/mixins/category-property-mixin.ts** adds the required property
**category** to any model.
{% include code-caption.html content="src/mixins/category-property-mixin.ts" %}
```ts
import {MixinTarget} from '/core';
import {property, Model} from '/repository';
/**
* A mixin factory to add `category` property
*
* @param superClass - Base Class
* @typeParam T - Model class
*/
export function AddCategoryPropertyMixin<T extends MixinTarget<Model>>(
superClass: T,
) {
class MixedModel extends superClass {
({
type: 'string',
required: true,
})
category: string;
}
return MixedModel;
}
```
{% include note.html content="At the moment, [TypeScript does not allow decorators in class expressions](https://github.com/microsoft/TypeScript/issues/7342). This is why we need to declare the class with a name, and then return it." %}
#### Generating A Model Via The CLI
A CLI-generated model named `Note` with 3 properties: **id**, **title**, and
**content** would look like this:
{% include code-caption.html content="src/models/note.model.ts" %}
```ts
import {Entity, model, property} from '/repository';
()
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;
```
#### Adjusting The Model File To Use AddCategoryPropertyMixin
The model file only requires a few adjustments:
- import the `BaseEntity` class
- import the `AddCategoryPropertyMixin` mixin
- Change the class declaration of `Note` so that it extends the class returned
from the mixin function which takes in the `BaseEntity` superclass as input
{% include code-caption.html content="src/models/note.model.ts" %}
```ts
import {model, property} from '/repository';
import {AddCategoryPropertyMixin} from '../mixins/category-property-mixin';
import {BaseEntity} from './base-entity';
()
export class Note extends AddCategoryPropertyMixin(BaseEntity) {
({
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;
```
The required property `category` has now been added to the `Note` model via a
mixin class factory function.
## Creating A Custom Model Method And Remote Model Method Mixin
This section covers the approach that LoopBack 3 can use to add a custom method
/remote method to a model via a mixin, and similarly how LoopBack 4 can add a
custom method to a repository and controller via a mixin.
### LoopBack 3 Approach
The
[Add a New Model Method And a New Endpoint](./methods.md#add-a-new-model-method-and-a-new-endpoint)
section of the [Migrating custom model methods](./methods.md) document explains
how a LoopBack 3 developer can define a custom model method named `findByTitle`
on the `Note` model, and define a remote method to make it available as a new
endpoint.
In this section, we will show how a LoopBack 3 developer can define a mixin to
accomplish this.
In LoopBack 3, a developer is able to create a custom model method/remote method
mixin by:
- placing the mixin logic in a file in a mixins directory
- updating the server/model-config.json file with the mixin directory location
- updating the model's json file to include the mixin's name (and options object
or boolean)
#### Defining The Model Method Mixin findByTitle.js
The developer defines a custom model method/remote method mixin in
**common/mixins/findByTitle.js** which adds a custom method `findByTitle` to any
model, and adds a corresponding remote method definition with path
`/findByTitle` as well. An options property `returnArgumentName` makes it
possible to customize the name of the return argument. If it is not specified,
the return argument of 'items' is used as a default.
{% include code-caption.html content="common/mixins/findByTitle.js" %}
```js
module.exports = function (Model, options) {
const returnArgumentName = options.returnArgumentName
? options.returnArgumentName
: 'items';
Model.remoteMethod('findByTitle', {
http: {
path: '/findByTitle',
verb: 'get',
},
accepts: {arg: 'title', type: 'string'},
returns: {arg: returnArgumentName, type: [Model], root: true},
});
Model.findByTitle = function (title, cb) {
var titleFilter = {
where: {
title: title,
},
};
Model.find(titleFilter, cb);
};
};
```
For a model named `Note`, this will expose an endpoint of `/Notes/findByTitle`.
Ensure **model-config.json** is set up properly as specified earlier in
[Updating model-config.json](#updating-model-config.json)
#### Applying The findByTitle.js Mixin To A Model
To extend the model `Note` with the **findByTitle.js** mixin, we need to add a
**mixins** section in **common/models/note.json** to indicate which mixins
should be applied to it.
{% include code-caption.html content="common/models/note.json" %}
```json
{
"name": "Note",
"properties": {
"title": {
"type": "string",
"required": true
},
"content": {
"type": "string"
}
},
"mixins": {
"FindByTitle": {
"returnArgumentName": "notes"
},
"Category": true
}
}
```
Specifying an options object for `FindByTitle` is the same as specifying a value
of **true** as it will apply the **findByTitle.js** custom model method/remote
method mixin to the `Note` model. A value of **false** will not apply the mixin.
### LoopBack 4 Approach
As mentioned in the previous section, the
[Add a New Model Method And a New Endpoint](./methods.md#add-a-new-model-method-and-a-new-endpoint)
section of the [Migrating custom model methods](./methods.md) document explains
how a LoopBack 3 developer can define a custom model method named `findByTitle`
on the `Note` model, and define a remote method to make it available as a new
endpoint. It then shows how a LoopBack 4 developer can implement a `findByTitle`
method on the `NoteRepository` and on the `NoteController` to accomplish the
same thing.
In this section, we will show how a LoopBack 4 developer can define two mixins (
a repository mixin and a controller mixin) to add a `findByTitle` method to
`NoteRepository` and `NoteController` respectively.
In LoopBack 4, a developer is able to create a repository and controller method
mixin by:
- defining a common interface for both mixin class factory functions
- placing the mixin class factory functions in separate files
- generating a repository and controller using the CLI as usual
- adjusting the repository and controller files to make use of its respective
mixin class factory function
#### Defining A Common Interface For The findByTitle Method
Let's define a common interface `FindByTitle` in
**src/mixins/find-by-title-interface.ts**.
{% include code-caption.html content="src/mixins/find-by-title-interface.ts" %}
```ts
import {Model} from '/repository';
/**
* An interface to allow finding notes by title
*/
export interface FindByTitle<M extends Model> {
findByTitle(title: string): Promise<M[]>;
}
```
#### Defining A Repository Mixin Class Factory Function
In **src/mixins/find-by-title-repository-mixin.ts**, let's define the mixin
class factory function `FindByTitleRepositoryMixin` which adds the `findByTitle`
method to any repository.
{% include code-caption.html content="src/mixins/find-by-title-repository-mixin.ts" %}
```ts
import {MixinTarget} from '/core';
import {Model, CrudRepository, Where} from '/repository';
import {FindByTitle} from './find-by-title-interface';
/*
* This function adds a new method 'findByTitle' to a repository class
* where 'M' is a model which extends Model
*
* @param superClass - Base class
*
* @typeParam M - Model class which extends Model
* @typeParam R - Repository class
*/
export function FindByTitleRepositoryMixin<
M extends Model & {title: string},
R extends MixinTarget<CrudRepository<M>>
>(superClass: R) {
class MixedRepository extends superClass implements FindByTitle<M> {
async findByTitle(title: string): Promise<M[]> {
const where = {title} as Where<M>;
const titleFilter = {where};
return this.find(titleFilter);
}
}
return MixedRepository;
}
```
#### Generating A Repository Via The CLI
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);
}
}
```
#### Adjusting NoteRepository To Use FindByTitleRepositoryMixin
The repository file only requires a few adjustments:
- import the `FindByTitleRepositoryMixin` mixin class factory function
- adjust the declaration of the `NoteRepository` class to extend the class
returned from the mixin function which takes in the `DefaultCrudRepository`
superclass as input.
{% include code-caption.html content="src/repositories/note.repository.ts" %}
```ts
import {FindByTitleRepositoryMixin} from '../mixins/find-by-title-repository-mixin';
import {DefaultCrudRepository} from '/repository';
import {Note, NoteRelations} from '../models';
import {DbDataSource} from '../datasources';
import {inject, Constructor} from '/core';
/**
* A repository for `Note` with `findByTitle`
*/
export class NoteRepository extends FindByTitleRepositoryMixin<
Note,
Constructor<
DefaultCrudRepository<Note, typeof Note.prototype.id, NoteRelations>
>
>(DefaultCrudRepository) {
constructor(('datasources.db') dataSource: DbDataSource) {
super(Note, dataSource);
}
}
```
We have now added the `findByTitle` method to a repository via a mixin class
factory function.
#### Defining A Controller Mixin Class Factory Function
In **src/mixins/find-by-title-controller-mixin.ts**, let's define the mixin
class factory function `FindByTitleControllerMixin` which adds the `findByTitle`
method to any controller.
{% include code-caption.html content="src/mixins/src/mixins/find-by-title-controller-mixin.ts" %}
```ts
import {MixinTarget} from '/core';
import {Model} from '/repository';
import {FindByTitle} from './find-by-title-interface';
import {param, get, getModelSchemaRef} from '/rest';
/**
* Options to mix in findByTitle
*/
export interface FindByTitleControllerMixinOptions {
/**
* Base path for the controller
*/
basePath: string;
/**
* Model class for CRUD
*/
modelClass: typeof Model;
}
/**
* A mixin factory for controllers to be extended by `FindByTitle`
* @param superClass - Base class
* @param options - Options for the controller
*
* @typeParam M - Model class
* @typeParam T - Base class
*/
export function FindByTitleControllerMixin<
M extends Model,
T extends MixinTarget<object>
>(superClass: T, options: FindByTitleControllerMixinOptions) {
class MixedController extends superClass implements FindByTitle<M> {
// Value will be provided by the subclassed controller class
repository: FindByTitle<M>;
(`${options.basePath}/findByTitle/{title}`, {
responses: {
'200': {
description: `Array of ${options.modelClass.modelName} model instances`,
content: {
'application/json': {
schema: {
type: 'array',
items: getModelSchemaRef(options.modelClass, {
includeRelations: true,
}),
},
},
},
},
},
})
async findByTitle(.path.string('title') title: string): Promise<M[]> {
return this.repository.findByTitle(title);
}
}
return MixedController;
}
```
To customize certain portions of the OpenAPI description of the endpoint, the
mixin class factory function needs to accept some options. We defined an
interface `FindByTitleControllerMixinOptions` to allow for this.
It is also a good idea to give the injected repository (in the controller super
class) a generic name like `this.repository` to keep things simple in the mixin
class factory function.
#### Generating A Controller Via The CLI
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/strongloop/loopback-next/blob/master/examples/todo/src/controllers/todo.controller.ts).
#### Adjusting NoteController To Use FindByTitleControllerMixin
The controller file only requires a few adjustments:
- import the `FindByTitleControllerMixinOptions` interface
- import the `FindByTitleControllerMixin` mixin class factory function
- prepare the options for the mixin
- adjust the declaration of the `NoteController` class to extend the class
returned from the mixin function which takes in the `Object` superclass as
input.
- pass the input options into the mixin
- change the name of the injected repository from `noteRepository` to
`repository` to keep things simple for the mixin class factory function
{% include code-caption.html content="src/controllers/note.controller.ts" %}
```ts
import {Note} from '../models';
import {
FindByTitleControllerMixin,
FindByTitleControllerMixinOptions,
} from '../mixins/find-by-title-controller-mixin';
import {Constructor} from '/core';
import {
Count,
CountSchema,
Filter,
repository,
Where,
} from '/repository';
import {
post,
param,
get,
getFilterSchemaFor,
getModelSchemaRef,
getWhereSchemaFor,
patch,
put,
del,
requestBody,
} from '/rest';
import {NoteRepository} from '../repositories';
const options: FindByTitleControllerMixinOptions = {
basePath: '/notes',
modelClass: Note,
};
export class NoteController extends FindByTitleControllerMixin<
Note,
Constructor<Object>
>(Object, options) {
constructor(
(NoteRepository)
public repository: NoteRepository,
) {
super();
}
('/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.repository.create(note);
}
// ...
// remaining CRUD endpoints
// ...
}
```
We have now added the `findByTitle` method to a controller via a mixin class
factory function.
This will also expose an endpoint of `/notes/findByTitle/{title}`.
## Summary
As the examples above show, migrating mixins from LoopBack 3 to LoopBack 4 is
relatively straightforward using class factory functions.