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

Add ObserverMixin to ModelBase typings #1823

Merged
merged 2 commits into from
Mar 2, 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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
coverage
dist
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ npm-debug.log
.travis.yml
.nyc_output
dist
types/__test__.ts
90 changes: 90 additions & 0 deletions types/__test__.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a comment that ModelBaseClass is an alias of typeof ModelBase and we are testing the alias here. thx.

Copy link
Member Author

Choose a reason for hiding this comment

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

See 6c38347, is my comment good enough?

Copy link
Contributor

Choose a reason for hiding this comment

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

The comment is great. thx. But a tiny typo: verifying that the value retunred

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 changes: 85 additions & 0 deletions types/model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -243,6 +244,90 @@ export declare class ModelBase {
anotherClass: string | ModelBaseClass | object,
options?: Options,
): ModelBaseClass;

emonddr marked this conversation as resolved.
Show resolved Hide resolved
// 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;
Expand Down
18 changes: 13 additions & 5 deletions types/observer-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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;

Expand Down Expand Up @@ -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).
Expand All @@ -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).
Expand Down