Tip: Missing instructions for your LoopBack 3 use case? Please report a Migration docs issue 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.
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.
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.
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
)
{
"_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.
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.
{
"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.
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.
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.
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;
}
Note: 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.
Generating A Model Via The CLI
A CLI-generated model named Note
with 3 properties: id, title, and
content would look like this:
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;
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 theBaseEntity
superclass as input
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.
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
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:
- 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.
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
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.
{
"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
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:
- 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.
import {Model} from '@loopback/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.
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;
}
Generating A Repository Via The CLI
A CLI-generated repository for a model Note
would look like this:
export class NoteRepository extends DefaultCrudRepository<
Note,
typeof Note.prototype.id,
NoteRelations
> {
constructor(@inject('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 theDefaultCrudRepository
superclass as input.
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.
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.
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.
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)
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 .
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 theObject
superclass as input. - pass the input options into the mixin
- change the name of the injected repository from
noteRepository
torepository
to keep things simple for the mixin class factory function
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}
.
Summary
As the examples above show, migrating mixins from LoopBack 3 to LoopBack 4 is relatively straightforward using class factory functions.