Skip to content

Commit

Permalink
feat: support operation hooks
Browse files Browse the repository at this point in the history
Add support for operation hooks.

Signed-off-by: Hage Yaapa <hage.yaapa@in.ibm.com>
  • Loading branch information
Hage Yaapa committed Mar 5, 2020
1 parent cd89ad4 commit 7444da9
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 8 deletions.
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;
}
}
```

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 after 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 => {
this.hooksCalled.push(beforeSave);
});

modelClass.observe(afterSave, async ctx => {
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
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);
}

/**
* 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

0 comments on commit 7444da9

Please sign in to comment.