docs/site/migration/models/mixins.md
{% include tip.html content=" Missing instructions for your LoopBack 3 use case? Please report a Migration docs issue on GitHub to let us know. " %}
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.
In LoopBack 3, it was easy to add property mixins and method mixins.
In LoopBack 4, it is also easy and is accomplished by using a mixin class factory function.
This section covers the approach that LoopBack 3 and LoopBack 4 can use to add a property to a model via a mixin.
In LoopBack 3, a developer is able to create a model property mixin by:
As an example, we will create a mixin that adds a category property to a model.
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" %}
module.exports = function (Model, options) {
Model.defineProperty('category', {type: 'string', required: true});
};
The server/model-config.json needs to contain:
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 for a short explanation of this file.
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" %}
{
"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.
In LoopBack 4, a developer is able to create a model property mixin by:
EntityLet'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" %}
import {Entity} from '@loopback/repository';
export class BaseEntity extends Entity {}
This is necessary because the Entity class is abstract and doesn't have a constructor.
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" %}
import {MixinTarget} from '@loopback/core';
import {property, Model} from '@loopback/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 {
@property({
type: 'string',
required: true,
})
category: string;
}
return MixedModel;
}
{% include note.html content="At the moment, TypeScript does not allow decorators in class expressions. This is why we need to declare the class with a name, and then return it." %}
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" %}
import {Entity, model, property} from '@loopback/repository';
@model()
export class Note extends Entity {
@property({
type: 'number',
id: true,
generated: true,
})
id?: number;
@property({
type: 'string',
required: true,
})
title: string;
@property({
type: 'string',
})
content?: string;
constructor(data?: Partial<Note>) {
super(data);
}
}
export interface NoteRelations {
// describe navigational properties here
}
export type NoteWithRelations = Note & NoteRelations;
The model file only requires a few adjustments:
BaseEntity classAddCategoryPropertyMixin mixinNote 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" %}
import {model, property} from '@loopback/repository';
import {AddCategoryPropertyMixin} from '../mixins/category-property-mixin';
import {BaseEntity} from './base-entity';
@model()
export class Note extends AddCategoryPropertyMixin(BaseEntity) {
@property({
type: 'number',
id: true,
generated: true,
})
id?: number;
@property({
type: 'string',
required: true,
})
title: string;
@property({
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.
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.
The
Add a New Model Method And a New Endpoint
section of the Migrating custom model methods 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:
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" %}
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
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" %}
{
"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.
As mentioned in the previous section, the
Add a New Model Method And a New Endpoint
section of the Migrating custom model methods 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:
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" %}
import {Model} from '@loopback/repository';
/**
* An interface to allow finding notes by title
*/
export interface FindByTitle<M extends Model> {
findByTitle(title: string): Promise<M[]>;
}
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" %}
import {MixinTarget} from '@loopback/core';
import {Model, CrudRepository, Where} from '@loopback/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;
}
A CLI-generated repository for a model Note would look like this:
{% include code-caption.html content="src/repositories/note.repository.ts" %}
export class NoteRepository extends DefaultCrudRepository<
Note,
typeof Note.prototype.id,
NoteRelations
> {
constructor(@inject('datasources.db') dataSource: DbDataSource) {
super(Note, dataSource);
}
}
The repository file only requires a few adjustments:
FindByTitleRepositoryMixin mixin class factory functionNoteRepository 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" %}
import {FindByTitleRepositoryMixin} from '../mixins/find-by-title-repository-mixin';
import {DefaultCrudRepository} from '@loopback/repository';
import {Note, NoteRelations} from '../models';
import {DbDataSource} from '../datasources';
import {inject, Constructor} from '@loopback/core';
/**
* A repository for `Note` with `findByTitle`
*/
export class NoteRepository extends FindByTitleRepositoryMixin<
Note,
Constructor<
DefaultCrudRepository<Note, typeof Note.prototype.id, NoteRelations>
>
>(DefaultCrudRepository) {
constructor(@inject('datasources.db') dataSource: DbDataSource) {
super(Note, dataSource);
}
}
We have now added the findByTitle method to a repository via a 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" %}
import {MixinTarget} from '@loopback/core';
import {Model} from '@loopback/repository';
import {FindByTitle} from './find-by-title-interface';
import {param, get, getModelSchemaRef} from '@loopback/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>;
@get(`${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(@param.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.
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" %}
export class NoteController {
constructor(
@repository(NoteRepository)
public noteRepository: NoteRepository,
) {}
@post('/notes', {
responses: {
'200': {
description: 'Note model instance',
content: {'application/json': {schema: getModelSchemaRef(Note)}},
},
},
})
async create(
@requestBody({
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 .
The controller file only requires a few adjustments:
FindByTitleControllerMixinOptions interfaceFindByTitleControllerMixin mixin class factory functionNoteController class to extend the class
returned from the mixin function which takes in the Object superclass as
input.noteRepository to
repository to keep things simple for the mixin class factory function{% include code-caption.html content="src/controllers/note.controller.ts" %}
import {Note} from '../models';
import {
FindByTitleControllerMixin,
FindByTitleControllerMixinOptions,
} from '../mixins/find-by-title-controller-mixin';
import {Constructor} from '@loopback/core';
import {
Count,
CountSchema,
Filter,
repository,
Where,
} from '@loopback/repository';
import {
post,
param,
get,
getFilterSchemaFor,
getModelSchemaRef,
getWhereSchemaFor,
patch,
put,
del,
requestBody,
} from '@loopback/rest';
import {NoteRepository} from '../repositories';
const options: FindByTitleControllerMixinOptions = {
basePath: '/notes',
modelClass: Note,
};
export class NoteController extends FindByTitleControllerMixin<
Note,
Constructor<Object>
>(Object, options) {
constructor(
@repository(NoteRepository)
public repository: NoteRepository,
) {
super();
}
@post('/notes', {
responses: {
'200': {
description: 'Note model instance',
content: {'application/json': {schema: getModelSchemaRef(Note)}},
},
},
})
async create(
@requestBody({
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}.
As the examples above show, migrating mixins from LoopBack 3 to LoopBack 4 is relatively straightforward using class factory functions.