Merge pull request #1823 from strongloop/feat/fix-observer-api

Add ObserverMixin to ModelBase typings
This commit is contained in:
Miroslav Bajtoš 2020-03-02 15:10:03 +01:00 committed by GitHub
commit 33884062f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 190 additions and 5 deletions

View File

@ -1 +1,2 @@
coverage
dist

View File

@ -12,3 +12,4 @@ npm-debug.log
.travis.yml
.nyc_output
dist
types/__test__.ts

90
types/__test__.ts Normal file
View File

@ -0,0 +1,90 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
// A test file to verify types described by our .d.ts files.
// The code in this file is only compiled, we don't run it via Mocha.
import {
DataSource,
KeyValueModel,
ModelBase,
ModelBaseClass,
PersistedModel,
PersistedModelClass,
} from '..';
const db = new DataSource('db', {connector: 'memory'});
//-------
// ModelBase should provide ObserverMixin APIs as static methods
//-------
//
(function() {
const Data = db.createModel('Data');
// An operation hook can be installed
Data.observe('before save', async ctx => {});
// Context is typed and provides `Model` property
Data.observe('before save', async ctx => {
console.log(ctx.Model.modelName);
});
// ModelBaseClass can be assigned to `typeof ModelBase`
// Please note that both `ModelBaseClass` and typeof ModelBase`
// are different ways how to describe a class constructor of a model.
// In this test we are verifying that the value returned by `createModel`
// can be assigned to both types.
const modelTypeof: typeof ModelBase = Data;
const modelCls: ModelBaseClass = modelTypeof;
});
//-------
// PersistedModel should provide ObserverMixin APIs as static methods
//-------
(function () {
const Product = db.createModel<PersistedModelClass>(
'Product',
{name: String},
{strict: true}
);
// It accepts async function
Product.observe('before save', async ctx => {});
// It accepts callback-based function
Product.observe('before save', (ctx, next) => {
next(new Error('test error'));
});
// ctx.Model is a PersistedModel class constructor
Product.observe('before save', async ctx => {
await ctx.Model.findOne();
});
// PersistedModelClass can be assigned to `typeof PersistedModel`
// Please note that both `PersistedModelClass` and typeof PersistedModel`
// are different ways how to describe a class constructor of a model.
// In this test we are verifying that the value returned by `createModel`
// can be assigned to both types.
const modelTypeof: typeof PersistedModel = Product;
const modelCls: PersistedModelClass = modelTypeof;
});
//-------
// KeyValueModel should provide ObserverMixin APIs as static methods
//-------
(function () {
const kvdb = new DataSource({connector: 'kv-memory'});
const CacheItem = kvdb.createModel<typeof KeyValueModel>('CacheItem');
// An operation hook can be installed
CacheItem.observe('before save', async ctx => {});
// ctx.Model is a KeyValueModel class constructor
CacheItem.observe('before save', async ctx => {
await ctx.Model.expire('key', 100);
});
});

85
types/model.d.ts vendored
View File

@ -6,6 +6,7 @@
import {EventEmitter} from 'events';
import {AnyObject, Options} from './common';
import {DataSource} from './datasource';
import {Listener, OperationHookContext} from './observer-mixin';
/**
* Property types
@ -243,6 +244,90 @@ export declare class ModelBase {
anotherClass: string | ModelBaseClass | object,
options?: Options,
): ModelBaseClass;
// ObserverMixin members are added as static methods, this is difficult to
// describe in TypeScript in a way that's easy to use by consumers.
// As a workaround, we include a copy of ObserverMixin members here.
//
// Ideally, we want to describe the context argument as
// `OperationHookContext<this>`. Unfortunately, that's not supported by
// TypeScript for static members. A nice workaround is described in
// https://github.com/microsoft/TypeScript/issues/5863#issuecomment-410887254
// - Describe the context using a generic argument `T`.
// - Use `this: T` argument to let the compiler infer what's the target
// model class we are going to observe.
/**
* Register an asynchronous observer for the given operation (event).
*
* Example:
*
* Registers a `before save` observer for a given model.
*
* ```javascript
* MyModel.observe('before save', function filterProperties(ctx, next) {
* if (ctx.options && ctx.options.skipPropertyFilter) return next();
* if (ctx.instance) {
* FILTERED_PROPERTIES.forEach(function(p) {
* ctx.instance.unsetAttribute(p);
* });
* } else {
* FILTERED_PROPERTIES.forEach(function(p) {
* delete ctx.data[p];
* });
* }
* next();
* });
* ```
*
* @param {String} operation The operation name.
* @callback {function} listener The listener function. It will be invoked with
* `this` set to the model constructor, e.g. `User`.
* @end
*/
static observe<T extends typeof ModelBase>(
this: T,
operation: string,
listener: Listener<OperationHookContext<T>>,
): void;
/**
* Unregister an asynchronous observer for the given operation (event).
*
* Example:
*
* ```javascript
* MyModel.removeObserver('before save', function removedObserver(ctx, next) {
* // some logic user want to apply to the removed observer...
* next();
* });
* ```
*
* @param {String} operation The operation name.
* @callback {function} listener The listener function.
* @end
*/
static removeObserver<T extends typeof ModelBase>(
this: T,
operation: string,
listener: Listener<OperationHookContext<T>>,
): Listener<OperationHookContext<T>> | undefined;
/**
* Unregister all asynchronous observers for the given operation (event).
*
* Example:
*
* Remove all observers connected to the `before save` operation.
*
* ```javascript
* MyModel.clearObservers('before save');
* ```
*
* @param {String} operation The operation name.
* @end
*/
static clearObservers(operation: string): void;
}
export type ModelBaseClass = typeof ModelBase;

View File

@ -4,9 +4,9 @@
// License text available at https://opensource.org/licenses/MIT
import {Callback, PromiseOrVoid} from './common';
import {PersistedModel, PersistedModelClass} from './persisted-model';
import {ModelBase} from './model';
export interface OperationHookContext<T extends typeof PersistedModel> {
export interface OperationHookContext<T extends typeof ModelBase> {
/**
* The constructor of the model that triggered the operation.
*/
@ -19,7 +19,7 @@ export interface OperationHookContext<T extends typeof PersistedModel> {
[property: string]: any;
}
export type Listener<Ctx = OperationHookContext<PersistedModelClass>> = (
export type Listener<Ctx = OperationHookContext<typeof ModelBase>> = (
ctx: Ctx, next: (err?: any) => void
) => void;
@ -52,7 +52,11 @@ export interface ObserverMixin {
* `this` set to the model constructor, e.g. `User`.
* @end
*/
observe(operation: string, listener: Listener<OperationHookContext<PersistedModelClass>>): void;
observe<T extends typeof ModelBase>(
this: T,
operation: string,
listener: Listener<OperationHookContext<T>>,
): void;
/**
* Unregister an asynchronous observer for the given operation (event).
@ -70,7 +74,11 @@ export interface ObserverMixin {
* @callback {function} listener The listener function.
* @end
*/
removeObserver(operation: string, listener: Listener<OperationHookContext<PersistedModelClass>>): Listener<OperationHookContext<PersistedModelClass>> | undefined;
removeObserver<T extends typeof ModelBase>(
this: T,
operation: string,
listener: Listener<OperationHookContext<T>>,
): Listener<OperationHookContext<T>> | undefined;
/**
* Unregister all asynchronous observers for the given operation (event).