diff --git a/docs/site/migration/components/models.md b/docs/site/migration/components/models.md index 705af60cb4bd..026d43e2e46f 100644 --- a/docs/site/migration/components/models.md +++ b/docs/site/migration/components/models.md @@ -6,8 +6,64 @@ sidebar: lb4_sidebar permalink: /doc/en/lb4/migration-extensions-models.html --- -{% include note.html content=" -This is a placeholder page, the task of adding content is tracked by the -following GitHub issue: -[loopback-next#3955](https://github.com/strongloop/loopback-next/issues/3955) -" %} +Please review the content in [Migrating models](../models/overview.md) first. +Going forward, we assume that you are already familiar with the differences +between LoopBack 3 Models and LoopBack 4 Entities & Repositories, and understand +how to migrate model-related functionality from a LoopBack 3 application to +LoopBack 4. + +## Import LoopBack 3 models + +The first step is to import LoopBack 3 model definitions into your LoopBack 4 +component. This will convert LB3 model JSON files into LB4 TypeScript classes, +as explained in +[Import Model definition](../models/core.md#import-model-definition) and +[Importing models from LoopBack 3 projects](../../Importing-LB3-models.md). + +1. Create a small LB3 app that is using your component. + +2. In your LB4 extension project, run `lb4 import-lb3-models ` + to import model(s) contributed by the component from the LB3 app you created + in the previous step. Change the base class of the imported model(s) from + `Entity` to `Model` if needed. + +## Migrate behavior-less models + +Sometimes, a LoopBack 3 model does not provide any behavior, it is just +describing the shape of data (e.g. data fields in a push notification object). +Such models can be converted to LoopBack 4 models as follows: + +1. Import the LoopBack 3 model to your LoopBack 4 project as explained in + [Import LoopBack 3 models](#import-loopback-3-models). + +2. Ensure that your component's main index file exports all models: + + ```ts + // src/index.ts + export * from './models'; + ``` + +3. Update your documentation to instruct users to import the models directly + from the extension, instead of relying on loopback-boot to pick them up. + + ```ts + import {MyModel} from 'my-extension'; + ``` + +4. Optionally, if you want your models to be injectable, add them to the + artifacts contributed by the extension. + + ```ts + import {MyModel} from './models'; + + export class MyComponent implements Component { + models: [MyModel]; + } + ``` + +## Advanced scenarios + +LoopBack 4 does not yet provide recipes for extensions sharing models together +with their persistence behavior and their REST APIs. Please join the discussion +in [loopback-next#5476](https://github.com/strongloop/loopback-next/issues/5476) +to let us know about your use cases and to subscribe for updates. diff --git a/docs/spikes/2020-04-how-to-migrate-lb3-components.md b/docs/spikes/2020-04-how-to-migrate-lb3-components.md index cb4a1dba055d..629495d551e4 100644 --- a/docs/spikes/2020-04-how-to-migrate-lb3-components.md +++ b/docs/spikes/2020-04-how-to-migrate-lb3-components.md @@ -220,7 +220,54 @@ See ### Migrate Models, Entities and Repositories -TBD +> Contribute custom models (Notification) describing shape of data expected by +> the services (Push service). These models are not backed by any datasource, +> they are primarily used to describe data fields. + +See +[Migrate behavior-less models](../site/migration/components/models.md#migrate-behavior-less-models). + +> Contribute custom entities (Application, Installation) to be persisted via +> CRUD, exposed via REST and possibly further customized by the app. +> Customization options include which datasource to use, the base path where +> REST API is exposed (e.g. `/api/apps` and `/api/devices`), additional fields +> (e.g. `Application.tenantId`) and changes in persistence behavior (e.g. via +> Operation Hooks) + +See +[loopback-next#5476](https://github.com/strongloop/loopback-next/issues/5476) + +> Add a custom Operation Hook to given models, with a config option to +> enable/disable this feature. The list of models can be provided explicitly in +> the component configuration or obtained dynamically via introspection (e.g. +> all models having a "belongsTo" relation with the Group model) + +See +[loopback-next#5476](https://github.com/strongloop/loopback-next/issues/5476) + +> Add new relations, e.g. between an app-provided entity `User` and a +> component-provided entity `File`. In this variant, the relation is added on +> small fixed number of models. + +See +[loopback-next#5476](https://github.com/strongloop/loopback-next/issues/5476) + +> A model mixing adding new relations (`hasMany ModelEvents`), installing +> Operation Hooks (to generate model events/audit log entries), adding new +> Repository APIs (for working with related model events). +> +> _(The mixin-based design may be difficult to accomplish in LB4, we may want to +> use introspection and a model setting instead. The trickier part is how to +> apply changes to models added after the component was mounted.)_ + +See +[loopback-next#5476](https://github.com/strongloop/loopback-next/issues/5476) + +> For all models with a flag enabled in model settings, setup a custom +> `afterRemote` hook to modify the HTTP response (e.g. add additional headers). + +See +[loopback-next#5476](https://github.com/strongloop/loopback-next/issues/5476) ### Migrate REST API diff --git a/packages/repository/src/__tests__/unit/mixins/repository.mixin.unit.ts b/packages/repository/src/__tests__/unit/mixins/repository.mixin.unit.ts index 05ee94b7cf0e..73bbad9a1486 100644 --- a/packages/repository/src/__tests__/unit/mixins/repository.mixin.unit.ts +++ b/packages/repository/src/__tests__/unit/mixins/repository.mixin.unit.ts @@ -11,6 +11,7 @@ import { DefaultCrudRepository, Entity, juggler, + Model, ModelDefinition, Repository, RepositoryMixin, @@ -77,6 +78,23 @@ describe('RepositoryMixin', () => { expectNoteRepoToBeBound(myApp); }); + it('binds user defined component with models', () => { + @model() + class MyModel extends Model {} + + class MyModelComponent { + models = [MyModel]; + } + + const myApp = new AppWithRepoMixin(); + myApp.component(MyModelComponent); + + const boundModels = myApp.find('models.*').map(b => b.key); + expect(boundModels).to.containEql('models.MyModel'); + const modelCtor = myApp.getSync('models.MyModel'); + expect(modelCtor).to.be.equal(MyModel); + }); + context('migrateSchema', () => { let app: AppWithRepoMixin; let migrateStub: sinon.SinonStub; diff --git a/packages/repository/src/mixins/repository.mixin.ts b/packages/repository/src/mixins/repository.mixin.ts index 247285eed191..92ad15a50081 100644 --- a/packages/repository/src/mixins/repository.mixin.ts +++ b/packages/repository/src/mixins/repository.mixin.ts @@ -221,28 +221,59 @@ export function RepositoryMixin>( nameOrOptions?: string | BindingFromClassOptions, ) { const binding = super.component(componentCtor, nameOrOptions); - this.mountComponentRepositories(componentCtor); + const instance = this.getSync(binding.key); + this.mountComponentRepositories(instance); + this.mountComponentModels(instance); return binding; } /** * Get an instance of a component and mount all it's * repositories. This function is intended to be used internally - * by component() + * by `component()`. * - * @param component - The component to mount repositories of + * NOTE: Calling `mountComponentRepositories` with a component class + * constructor is deprecated. You should instantiate the component + * yourself and provide the component instance instead. + * + * @param componentInstanceOrClass - The component to mount repositories of + * @internal */ - mountComponentRepositories(component: Class) { - const componentKey = `${CoreBindings.COMPONENTS}.${component.name}`; - const compInstance = this.getSync<{ - repositories?: Class>[]; - }>(componentKey); + mountComponentRepositories( + // accept also component class to preserve backwards compatibility + // TODO(semver-major) Remove support for component class constructor + componentInstanceOrClass: Class | RepositoryComponent, + ) { + const component = resolveComponentInstance(this); - if (compInstance.repositories) { - for (const repo of compInstance.repositories) { + if (component.repositories) { + for (const repo of component.repositories) { this.repository(repo); } } + + // `Readonly` is a hack to remove protected members + // and thus allow `this` to be passed as a value for `ctx` + function resolveComponentInstance(ctx: Readonly) { + if (typeof componentInstanceOrClass !== 'function') + return componentInstanceOrClass; + + const componentName = componentInstanceOrClass.name; + const componentKey = `${CoreBindings.COMPONENTS}.${componentName}`; + return ctx.getSync(componentKey); + } + } + + /** + * Bind all model classes provided by a component. + * @param component + * @internal + */ + mountComponentModels(component: RepositoryComponent) { + if (!component.models) return; + for (const m of component.models) { + this.model(m); + } } /** @@ -287,6 +318,24 @@ export function RepositoryMixin>( }; } +/** + * This interface describes additional Component properties + * allowing components to contribute Repository-related artifacts. + */ +export interface RepositoryComponent { + /** + * An optional list of Repository classes to bind for dependency injection + * via `app.repository()` API. + */ + repositories?: Class>[]; + + /** + * An optional list of Model classes to bind for dependency injection + * via `app.model()` API. + */ + models?: Class[]; +} + /** * Normalize name or options to `BindingFromClassOptions` * @param nameOrOptions - Name or options for binding from class