UNPKG

@loopback/docs

Version:

Documentation files rendered at [https://loopback.io](https://loopback.io)

743 lines (597 loc) 20.9 kB
--- 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 '@loopback/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 '@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](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 '@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 the `BaseEntity` superclass as input {% include code-caption.html content="src/models/note.model.ts" %} ```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. ## 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 '@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. {% include code-caption.html content="src/mixins/find-by-title-repository-mixin.ts" %} ```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; } ``` #### 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(@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 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 '@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. {% include code-caption.html content="src/mixins/src/mixins/find-by-title-controller-mixin.ts" %} ```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. #### 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( @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 ](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 '@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.