Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support operation hooks #4730

Merged
merged 1 commit into from
Mar 5, 2020
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
48 changes: 43 additions & 5 deletions docs/site/migration/models/operation-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,46 @@ sidebar: lb4_sidebar
permalink: /doc/en/lb4/migration-models-operation-hooks.html
---

{% include note.html content="
This is a placeholder page, the task of adding content is tracked by the
following GitHub issue:
[loopback-next#3952](https://github.com/strongloop/loopback-next/issues/3952)
" %}
Operation hooks are not supported in LoopBack 4 yet. See the
[Operation hooks for models/repositories spike](https://github.com/strongloop/loopback-next/issues/1919)
to follow the progress made on this subject.

In the meantime, we are providing a temporary API for enabling operation hooks
in LoopBack 4: override `DefaultCrudRepository`'s `definePersistedModel` method
in the model's repository.

The `definePersistedModel` method of `DefaultCrudRepository` returns a model
class on which you can apply the
[LoopBack 3 operation hooks](https://loopback.io/doc/en/lb3/Operation-hooks.html).
Make sure to return the model class from your repository's
`definePersistedModel` method.

Here is an example of a repository implementing `definePersistedModel` and
applying an operation hook on a model:

```ts
class ProductRepository extends DefaultCrudRepository<
Product,
typeof Product.prototype.id,
ProductRelations
> {
constructor(dataSource: juggler.DataSource) {
super(Product, dataSource);
}

definePersistedModel(entityClass: typeof Product) {
const modelClass = super.definePersistedModel(entityClass);
modelClass.observe('before save', async ctx => {
console.log(`going to save ${ctx.Model.modelName}`);
});
return modelClass;
}
}
```
bajtos marked this conversation as resolved.
Show resolved Hide resolved

Although possible, we are not providing an API which directly exposes the
`observe` method of the model class. The current API makes the registration of
operation hooks a process that is possible only at the time when the model class
is attached to the repository and accidental registration of the same operation
hook multiple times becomes obvious. With an API which directly exposes the
`observe` method of the model class, this would not have been possible.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/repository
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {DataSource} from 'loopback-datasource-juggler';
import {DefaultCrudRepository, juggler} from '../..';
import {Product, ProductRelations} from '../fixtures/models/product.model';

// This test shows the recommended way how to use @loopback/repository
// together with existing connectors when building LoopBack applications
describe('Operation hooks', () => {
let repo: ProductRepository;
beforeEach(givenProductRepository);

const beforeSave = 'before save';
const afterSave = 'after save';
const expectedArray = [beforeSave, afterSave];

it('supports operation hooks', async () => {
await repo.create({slug: 'pencil'});
expect(repo.hooksCalled).to.eql(expectedArray);
});

function givenProductRepository() {
const db = new DataSource({
connector: 'memory',
});

repo = new ProductRepository(db);
}

class ProductRepository extends DefaultCrudRepository<
Product,
typeof Product.prototype.id,
ProductRelations
> {
constructor(dataSource: juggler.DataSource) {
super(Product, dataSource);
}

hooksCalled: string[] = [];

definePersistedModel(entityClass: typeof Product) {
const modelClass = super.definePersistedModel(entityClass);
modelClass.observe(beforeSave, async ctx => {
Copy link
Member

Choose a reason for hiding this comment

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

code linter is complaining about Promise returned in function argument where a void return was expected. It might be because we're waiting for your changes in juggler to be released: loopbackio/loopback-datasource-juggler#1820.

this.hooksCalled.push(beforeSave);
});

modelClass.observe(afterSave, async ctx => {
Copy link
Member

Choose a reason for hiding this comment

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

code linter is complaining about Promise returned in function argument where a void return was expected. It might be because we're waiting for your changes in juggler to be released: loopbackio/loopback-datasource-juggler#1820.

this.hooksCalled.push(afterSave);
});
return modelClass;
}
}
});
21 changes: 18 additions & 3 deletions packages/repository/src/repositories/legacy-juggler-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ export class DefaultCrudRepository<
`Entity ${entityClass.name} must have at least one id/pk property.`,
);

this.modelClass = this.definePersistedModel(entityClass);
this.modelClass = this.ensurePersistedModel(entityClass);
}

// Create an internal legacy Model attached to the datasource
bajtos marked this conversation as resolved.
Show resolved Hide resolved
private definePersistedModel(
private ensurePersistedModel(
entityClass: typeof Model,
): typeof juggler.PersistedModel {
const definition = entityClass.definition;
Expand All @@ -148,6 +148,21 @@ export class DefaultCrudRepository<
return model as typeof juggler.PersistedModel;
}

return this.definePersistedModel(entityClass);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don’t we emit an event when definePersistedModel is done so that listeners can be registered to set up operation hooks? Overriding a method is always more advanced for a lot of developers.

Copy link
Contributor

Choose a reason for hiding this comment

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

For the record, we briefly discussed if it's feasible to use events to set up operation hooks and decided to defer that.

}

/**
* Creates a legacy persisted model class, attaches it to the datasource and
* returns it. This method can be overriden in sub-classes to acess methods
* and properties in the generated model class.
* @param entityClass - LB4 Entity constructor
*/
protected definePersistedModel(
entityClass: typeof Model,
): typeof juggler.PersistedModel {
const dataSource = this.dataSource;
const definition = entityClass.definition;

// To handle circular reference back to the same model,
// we create a placeholder model that will be replaced by real one later
dataSource.getModel(definition.name, true /* forceCreate */);
Expand Down Expand Up @@ -192,7 +207,7 @@ export class DefaultCrudRepository<
private resolvePropertyType(type: PropertyType): PropertyType {
const resolved = resolveType(type);
return isModelClass(resolved)
? this.definePersistedModel(resolved)
? this.ensurePersistedModel(resolved)
: resolved;
}

Expand Down