Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 61 additions & 5 deletions docs/site/migration/components/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path-to-lb3-app>`
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.
49 changes: 48 additions & 1 deletion docs/spikes/2020-04-how-to-migrate-lb3-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DefaultCrudRepository,
Entity,
juggler,
Model,
ModelDefinition,
Repository,
RepositoryMixin,
Expand Down Expand Up @@ -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<typeof MyModel>('models.MyModel');
expect(modelCtor).to.be.equal(MyModel);
});

context('migrateSchema', () => {
let app: AppWithRepoMixin;
let migrateStub: sinon.SinonStub;
Expand Down
69 changes: 59 additions & 10 deletions packages/repository/src/mixins/repository.mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,28 +221,59 @@ export function RepositoryMixin<T extends MixinTarget<Application>>(
nameOrOptions?: string | BindingFromClassOptions,
) {
const binding = super.component(componentCtor, nameOrOptions);
this.mountComponentRepositories(componentCtor);
const instance = this.getSync<C & RepositoryComponent>(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<unknown>) {
const componentKey = `${CoreBindings.COMPONENTS}.${component.name}`;
const compInstance = this.getSync<{
repositories?: Class<Repository<Model>>[];
}>(componentKey);
mountComponentRepositories(
// accept also component class to preserve backwards compatibility
// TODO(semver-major) Remove support for component class constructor
componentInstanceOrClass: Class<unknown> | 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<Application>` is a hack to remove protected members
// and thus allow `this` to be passed as a value for `ctx`
function resolveComponentInstance(ctx: Readonly<Application>) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot use a private method in mixins :(

packages/repository/src/mixins/repository.mixin.ts:42:17 - error TS4094: 
  Property '_resolveComponentInstance' of exported class expression may not be private or protected.

42 export function RepositoryMixin<T extends MixinTarget<Application>>(
                   ~~~~~~~~~~~~~~~

if (typeof componentInstanceOrClass !== 'function')
return componentInstanceOrClass;

const componentName = componentInstanceOrClass.name;
const componentKey = `${CoreBindings.COMPONENTS}.${componentName}`;
return ctx.getSync<RepositoryComponent>(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);
}
}

/**
Expand Down Expand Up @@ -287,6 +318,24 @@ export function RepositoryMixin<T extends MixinTarget<Application>>(
};
}

/**
* 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<Repository<Model>>[];

/**
* An optional list of Model classes to bind for dependency injection
* via `app.model()` API.
*/
models?: Class<Model>[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to add dataSources?

Copy link
Member Author

@bajtos bajtos May 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure. DataSources typically require environment-specific configuration (e.g. database connection string), I am no sure if it makes sense to export them from a component. Either way, such change is out of scope of this pull request - feel free to open a follow-up GH issue to discuss this idea more.

}

/**
* Normalize name or options to `BindingFromClassOptions`
* @param nameOrOptions - Name or options for binding from class
Expand Down