From e30f89676d0e450ab7f65daa84c4c63a5b88034c Mon Sep 17 00:00:00 2001 From: Hage Yaapa Date: Mon, 24 Feb 2020 20:09:18 +0530 Subject: [PATCH] feat: support operation hooks Add support for operation hooks. Signed-off-by: Hage Yaapa --- docs/site/migration/models/operation-hooks.md | 48 ++++++++++++++-- .../acceptance/operation-hooks.acceptance.ts | 57 +++++++++++++++++++ .../src/repositories/legacy-juggler-bridge.ts | 21 ++++++- 3 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 packages/repository/src/__tests__/acceptance/operation-hooks.acceptance.ts diff --git a/docs/site/migration/models/operation-hooks.md b/docs/site/migration/models/operation-hooks.md index 9896b070920d..2289ff4667d2 100644 --- a/docs/site/migration/models/operation-hooks.md +++ b/docs/site/migration/models/operation-hooks.md @@ -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 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. diff --git a/packages/repository/src/__tests__/acceptance/operation-hooks.acceptance.ts b/packages/repository/src/__tests__/acceptance/operation-hooks.acceptance.ts new file mode 100644 index 000000000000..9e10f5a698b9 --- /dev/null +++ b/packages/repository/src/__tests__/acceptance/operation-hooks.acceptance.ts @@ -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; + } + } +}); diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index bdd17c4ea1b1..cfa6f75d5631 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -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; @@ -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 */); @@ -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; }