diff --git a/.eslintignore b/.eslintignore index 4ebc8aea5..29f44348c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ coverage +dist diff --git a/.npmignore b/.npmignore index 0718b8a28..6a667c22e 100644 --- a/.npmignore +++ b/.npmignore @@ -12,3 +12,4 @@ npm-debug.log .travis.yml .nyc_output dist +types/__test__.ts diff --git a/types/__test__.ts b/types/__test__.ts new file mode 100644 index 000000000..6d0d1bfca --- /dev/null +++ b/types/__test__.ts @@ -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( + '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('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); + }); +}); diff --git a/types/model.d.ts b/types/model.d.ts index 9543ec366..7b15a9d6e 100644 --- a/types/model.d.ts +++ b/types/model.d.ts @@ -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`. 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( + this: T, + operation: string, + listener: Listener>, + ): 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( + this: T, + operation: string, + listener: Listener>, + ): Listener> | 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; diff --git a/types/observer-mixin.d.ts b/types/observer-mixin.d.ts index baf21333a..81eb768b8 100644 --- a/types/observer-mixin.d.ts +++ b/types/observer-mixin.d.ts @@ -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 { +export interface OperationHookContext { /** * The constructor of the model that triggered the operation. */ @@ -19,7 +19,7 @@ export interface OperationHookContext { [property: string]: any; } -export type Listener> = ( +export type Listener> = ( 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>): void; + observe( + this: T, + operation: string, + listener: Listener>, + ): 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>): Listener> | undefined; + removeObserver( + this: T, + operation: string, + listener: Listener>, + ): Listener> | undefined; /** * Unregister all asynchronous observers for the given operation (event).