@poppinss/hooks
Advanced tools
Comparing version 5.0.0 to 6.0.0-0
@@ -0,1 +1,2 @@ | ||
export * from './src/Contracts'; | ||
export { Hooks } from './src/Hooks'; |
@@ -10,5 +10,16 @@ "use strict"; | ||
*/ | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Hooks = void 0; | ||
__exportStar(require("./src/Contracts"), exports); | ||
var Hooks_1 = require("./src/Hooks"); | ||
Object.defineProperty(exports, "Hooks", { enumerable: true, get: function () { return Hooks_1.Hooks; } }); |
@@ -1,59 +0,51 @@ | ||
/// <reference types="@adonisjs/application/build/adonis-typings/application" /> | ||
declare type HooksHandler = (...args: any[]) => void | Promise<void>; | ||
import { Runner } from '../Runner'; | ||
import { HooksHandler } from '../Contracts'; | ||
/** | ||
* Exposes the API to register before/after lifecycle hooks for a given action | ||
* with option to resolve handlers from the IoC container. | ||
* Exposes the API for registering hooks for a given lifecycle action. | ||
* | ||
* The hooks class doesn't provide autocomplete for actions and the arguments | ||
* the handler will receive, since we expect this class to be used internally | ||
* for user facing objects. | ||
* @example | ||
* const hooks = new Hooks() | ||
* hooks.add('onCreate', function () { | ||
* doSomething | ||
* }) | ||
*/ | ||
export declare class Hooks { | ||
private resolver?; | ||
private hooks; | ||
constructor(resolver?: import("@ioc:Adonis/Core/Application").IocResolverContract<import("@ioc:Adonis/Core/Application").ContainerBindings> | undefined); | ||
/** | ||
* Raise exceptins when resolver is not defined | ||
* Pre-resolved registered hooks | ||
*/ | ||
private ensureResolver; | ||
private hooks; | ||
/** | ||
* Resolves the hook handler using the resolver when it is defined as string | ||
* or returns the function reference back | ||
* Add handler to the actions map | ||
*/ | ||
private resolveHandler; | ||
private addHandler; | ||
/** | ||
* Returns handlers set for a given action or undefined | ||
* Returns a map of registered hooks | ||
*/ | ||
private getActionHandlers; | ||
all(): Map<string, Set<HooksHandler>>; | ||
/** | ||
* Adds the resolved handler to the actions set | ||
* Returns true when a handler has already been registered | ||
*/ | ||
private addResolvedHandler; | ||
has(action: string, handler: HooksHandler): boolean; | ||
/** | ||
* Returns a boolean whether a handler has been already registered or not | ||
* Register handler for a given action | ||
*/ | ||
has(lifecycle: 'before' | 'after', action: string, handler: HooksHandler | string): boolean; | ||
add(action: string, handler: HooksHandler): this; | ||
/** | ||
* Register hook handler for a given event and lifecycle | ||
*/ | ||
add(lifecycle: 'before' | 'after', action: string, handler: HooksHandler | string): this; | ||
/** | ||
* Remove a pre-registered handler | ||
*/ | ||
remove(lifecycle: 'before' | 'after', action: string, handler: HooksHandler | string): void; | ||
remove(action: string, handler: HooksHandler): void; | ||
/** | ||
* Remove all handlers for a given action or lifecycle. If action is not | ||
* defined, then all actions for that given lifecycle are removed | ||
* Remove all handlers for a given action. If action is not | ||
* defined, then all actions are removed. | ||
*/ | ||
clear(lifecycle: 'before' | 'after', action?: string): void; | ||
clear(action?: string): void; | ||
/** | ||
* Merges hooks of a given hook instance. To merge from more than | ||
* one instance, you can call the merge method for multiple times | ||
* Merge pre-resolved hooks from an existing | ||
* hooks instance | ||
*/ | ||
merge(hooks: Hooks): void; | ||
/** | ||
* Executes the hook handler for a given action and lifecycle | ||
* Returns an instance of hooks runner. Optionally, a few hooks can be disabled. | ||
*/ | ||
exec(lifecycle: 'before' | 'after', action: string, ...data: any[]): Promise<void>; | ||
runner(action: string, withoutHooks?: string[]): Runner; | ||
} | ||
export {}; |
@@ -12,70 +12,50 @@ "use strict"; | ||
exports.Hooks = void 0; | ||
const Runner_1 = require("../Runner"); | ||
/** | ||
* Exposes the API to register before/after lifecycle hooks for a given action | ||
* with option to resolve handlers from the IoC container. | ||
* Exposes the API for registering hooks for a given lifecycle action. | ||
* | ||
* The hooks class doesn't provide autocomplete for actions and the arguments | ||
* the handler will receive, since we expect this class to be used internally | ||
* for user facing objects. | ||
* @example | ||
* const hooks = new Hooks() | ||
* hooks.add('onCreate', function () { | ||
* doSomething | ||
* }) | ||
*/ | ||
class Hooks { | ||
constructor(resolver) { | ||
this.resolver = resolver; | ||
this.hooks = { | ||
before: new Map(), | ||
after: new Map(), | ||
}; | ||
constructor() { | ||
/** | ||
* Pre-resolved registered hooks | ||
*/ | ||
this.hooks = new Map(); | ||
} | ||
/** | ||
* Raise exceptins when resolver is not defined | ||
* Add handler to the actions map | ||
*/ | ||
ensureResolver() { | ||
if (!this.resolver) { | ||
throw new Error('IoC container resolver is required to register string based hooks handlers'); | ||
addHandler(action, handler) { | ||
const handlers = this.hooks.get(action); | ||
if (!handlers) { | ||
this.hooks.set(action, new Set()); | ||
} | ||
this.hooks.get(action).add(handler); | ||
} | ||
/** | ||
* Resolves the hook handler using the resolver when it is defined as string | ||
* or returns the function reference back | ||
* Returns a map of registered hooks | ||
*/ | ||
resolveHandler(handler) { | ||
if (typeof handler === 'string') { | ||
this.ensureResolver(); | ||
return this.resolver.resolve(handler); | ||
} | ||
return handler; | ||
all() { | ||
return this.hooks; | ||
} | ||
/** | ||
* Returns handlers set for a given action or undefined | ||
* Returns true when a handler has already been registered | ||
*/ | ||
getActionHandlers(lifecycle, action) { | ||
return this.hooks[lifecycle].get(action); | ||
} | ||
/** | ||
* Adds the resolved handler to the actions set | ||
*/ | ||
addResolvedHandler(lifecycle, action, handler) { | ||
const handlers = this.getActionHandlers(lifecycle, action); | ||
if (handlers) { | ||
handlers.add(handler); | ||
} | ||
else { | ||
this.hooks[lifecycle].set(action, new Set([handler])); | ||
} | ||
} | ||
/** | ||
* Returns a boolean whether a handler has been already registered or not | ||
*/ | ||
has(lifecycle, action, handler) { | ||
const handlers = this.getActionHandlers(lifecycle, action); | ||
has(action, handler) { | ||
const handlers = this.hooks.get(action); | ||
if (!handlers) { | ||
return false; | ||
} | ||
return handlers.has(this.resolveHandler(handler)); | ||
return handlers.has(handler); | ||
} | ||
/** | ||
* Register hook handler for a given event and lifecycle | ||
* Register handler for a given action | ||
*/ | ||
add(lifecycle, action, handler) { | ||
this.addResolvedHandler(lifecycle, action, this.resolveHandler(handler)); | ||
add(action, handler) { | ||
this.addHandler(action, handler); | ||
return this; | ||
@@ -86,54 +66,38 @@ } | ||
*/ | ||
remove(lifecycle, action, handler) { | ||
const handlers = this.getActionHandlers(lifecycle, action); | ||
remove(action, handler) { | ||
const handlers = this.hooks.get(action); | ||
if (!handlers) { | ||
return; | ||
} | ||
handlers.delete(this.resolveHandler(handler)); | ||
handlers.delete(handler); | ||
} | ||
/** | ||
* Remove all handlers for a given action or lifecycle. If action is not | ||
* defined, then all actions for that given lifecycle are removed | ||
* Remove all handlers for a given action. If action is not | ||
* defined, then all actions are removed. | ||
*/ | ||
clear(lifecycle, action) { | ||
clear(action) { | ||
if (!action) { | ||
this.hooks[lifecycle].clear(); | ||
this.hooks.clear(); | ||
return; | ||
} | ||
this.hooks[lifecycle].delete(action); | ||
this.hooks.delete(action); | ||
} | ||
/** | ||
* Merges hooks of a given hook instance. To merge from more than | ||
* one instance, you can call the merge method for multiple times | ||
* Merge pre-resolved hooks from an existing | ||
* hooks instance | ||
*/ | ||
merge(hooks) { | ||
hooks.hooks.before.forEach((actionHooks, action) => { | ||
hooks.all().forEach((actionHooks, action) => { | ||
actionHooks.forEach((handler) => { | ||
this.addResolvedHandler('before', action, handler); | ||
this.add(action, handler); | ||
}); | ||
}); | ||
hooks.hooks.after.forEach((actionHooks, action) => { | ||
actionHooks.forEach((handler) => { | ||
this.addResolvedHandler('after', action, handler); | ||
}); | ||
}); | ||
} | ||
/** | ||
* Executes the hook handler for a given action and lifecycle | ||
* Returns an instance of hooks runner. Optionally, a few hooks can be disabled. | ||
*/ | ||
async exec(lifecycle, action, ...data) { | ||
const handlers = this.getActionHandlers(lifecycle, action); | ||
if (!handlers) { | ||
return; | ||
} | ||
for (let handler of handlers) { | ||
if (typeof handler === 'function') { | ||
await handler(...data); | ||
} | ||
else { | ||
await this.resolver.call(handler, undefined, data); | ||
} | ||
} | ||
runner(action, withoutHooks) { | ||
return new Runner_1.Runner(this.hooks.get(action), withoutHooks); | ||
} | ||
} | ||
exports.Hooks = Hooks; |
{ | ||
"name": "@poppinss/hooks", | ||
"version": "5.0.0", | ||
"version": "6.0.0-0", | ||
"description": "A no brainer hooks module for execute before/after lifecycle hooks", | ||
@@ -11,2 +11,5 @@ "main": "build/index.js", | ||
], | ||
"exports": { | ||
".": "./build/index.js" | ||
}, | ||
"scripts": { | ||
@@ -27,12 +30,3 @@ "mrm": "mrm --preset=@adonisjs/mrm-preset", | ||
}, | ||
"peerDependencies": { | ||
"@adonisjs/application": ">=4.0.0" | ||
}, | ||
"peerDependenciesMeta": { | ||
"@adonisjs/application": { | ||
"optional": true | ||
} | ||
}, | ||
"devDependencies": { | ||
"@adonisjs/application": "^5.1.8", | ||
"@adonisjs/mrm-preset": "^5.0.2", | ||
@@ -79,7 +73,2 @@ "@adonisjs/require-ts": "^2.0.7", | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" | ||
} | ||
}, | ||
"config": { | ||
@@ -136,3 +125,7 @@ "commitizen": { | ||
"printWidth": 100 | ||
}, | ||
"publishConfig": { | ||
"access": "public", | ||
"tag": "next" | ||
} | ||
} |
149
README.md
@@ -18,8 +18,6 @@ <div align="center"><img src="https://res.cloudinary.com/adonisjs/image/upload/q_100/v1557762307/poppinss_iftxlt.jpg" width="600px"></div> | ||
- [Usage](#usage) | ||
- [API](#api) | ||
- [add(lifecycle: 'before' | 'after', action: string, handler: Function | string)](#addlifecycle-before--after-action-string-handler-function--string) | ||
- [exec(lifecycle: 'before' | 'after', action: string, ...data: any[])](#execlifecycle-before--after-action-string-data-any) | ||
- [remove (lifecycle: 'before' | 'after', action: string, handler: HooksHandler | string)](#remove-lifecycle-before--after-action-string-handler-hookshandler--string) | ||
- [clear(lifecycle: 'before' | 'after', action?: string)](#clearlifecycle-before--after-action-string) | ||
- [merge (hooks: Hooks): void](#merge-hooks-hooks-void) | ||
- [Removing `before` and `after` calls with cleanup functions](#removing-before-and-after-calls-with-cleanup-functions) | ||
- [Scanerio with hooks](#scanerio-with-hooks) | ||
- [Scanerio with cleanup functions](#scanerio-with-cleanup-functions) | ||
- [Passing data to hooks](#passing-data-to-hooks) | ||
@@ -32,6 +30,4 @@ <!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
For example: The Lucid models uses this class internally and expose `before` and `after` methods on the model itself. Doing this, Lucid can control the autocomplete, type checking for the `before` and `after` methods itself, without relying on this package to expose the generics API. | ||
For example: The Luciod models uses this internally and exposes the API to register lifecycle hooks around model actions like `creating`, `created`, `deleting`, `deleted` and so on. | ||
> Also generics increases the number of types Typescript has to generate and it's better to avoid them whenever possible. | ||
## Installation | ||
@@ -42,6 +38,6 @@ | ||
```sh | ||
npm i @poppinss/hooks | ||
npm i @poppinss/hooks@next | ||
# yarn | ||
yarn add @poppinss/hooks | ||
yarn add @poppinss/hooks@next | ||
``` | ||
@@ -57,80 +53,119 @@ | ||
hooks.add('before', 'save', function () {}) | ||
hooks.add('creating', function () {}) | ||
hooks.add('created', function () {}) | ||
// Later invoke before save hooks | ||
await hooks.exec('before', 'save', { id: 1 }) | ||
const runner = hooks.runner('creating') | ||
try { | ||
await runner.run() // pass data here | ||
} finally { | ||
await runner.cleaup() // pass data here | ||
} | ||
``` | ||
If you want the end user to define IoC container bindings as the hook handler, then you need to pass the `IoC` container resolver to the Hooks constructor. Following is the snippet from Lucid models. | ||
## Removing `before` and `after` calls with cleanup functions | ||
Usually hooks are modeled around `before` and `after` events. Also, the previous versions of this package also allowed registering before and after lifecycle hooks. However, I have recently removed support for that because of the following reasons. | ||
The `before` and `after` lifecycle hooks will always be prone to errors. Let's consider the following scanerio. | ||
#### Scanerio with hooks | ||
We have the following `before` hooks. | ||
```ts | ||
import { Ioc } from '@adonisjs/fold' | ||
const ioc = new Ioc() | ||
const resolver = ioc.getResolver(undefined, 'modelHooks', 'App/Models/Hooks') | ||
hooks.add('before', 'save', function () { | ||
createTemporaryResource() | ||
}) | ||
const hooks = new Hooks(resolver) | ||
``` | ||
hooks.add('before', 'save', function () { | ||
createAnotherTemporaryResource() | ||
}) | ||
The resolver allows the end user to pass the hook reference as string and hooks must live inside `App/Models/Hooks` folder. | ||
hooks.add('after', 'save', function () { | ||
removeFirstTemporaryResource() | ||
}) | ||
```ts | ||
hooks.add('before', 'save', 'User.encryptPassword') | ||
hooks.add('after', 'save', function () { | ||
removeSecondTemporaryResource() | ||
}) | ||
``` | ||
## API | ||
**Now let's save the operation around which the lifecycle hooks were registered fails. Should we fire the `after` hooks?** | ||
#### add(lifecycle: 'before' | 'after', action: string, handler: Function | string) | ||
- If no, then temporary resources will be never be removed | ||
- If yes, then we will end up in the stable state. | ||
Add a new hook handler. | ||
**However, what happens if the second before hook fails?** Now should we call all the `after` hooks or not? | ||
```ts | ||
hooks.add('before', 'save', (data) => { | ||
console.log(data) | ||
}) | ||
``` | ||
- If yes, then the `removeSecondTemporaryResource` may error out since its `before` action was never completed. | ||
- If not, then again we will end up in a dirty state from the first `before` hook. | ||
#### exec(lifecycle: 'before' | 'after', action: string, ...data: any[]) | ||
As I mentioned earlier, the `before` and `after` hooks have no direct relationship and hence there is no correct way to call only the `after` hooks for which the `before` hooks completed successfully. | ||
Execute a given hook for a selected lifecycle. | ||
Therefore, by removing the concept of `before` and `after` and using cleanup functions, we will be able to design a more robust hooks system. | ||
### Scanerio with cleanup functions | ||
Now, in the following API, the hooks themselves are responsible for returning the cleanup functions. | ||
If the second hook fails, then we will only call the cleanup function for the first hook. | ||
```ts | ||
hooks.exec('before', 'save', { username: 'virk' }) | ||
hooks.add('save', function () { | ||
createTemporaryResource() | ||
return removeFirstTemporaryResource | ||
}) | ||
hooks.add('save', function () { | ||
createAnotherTemporaryResource() | ||
return removeSecondTemporaryResource | ||
}) | ||
``` | ||
#### remove (lifecycle: 'before' | 'after', action: string, handler: HooksHandler | string) | ||
## Passing data to hooks | ||
You can pass data to hooks at the time of running the `run` and the `cleanup` functions. For example. | ||
Remove an earlier registered hook. If you are using the IoC container bindings, then passing the binding string is enough, otherwise you need to store the reference of the function. | ||
```ts | ||
function onSave() {} | ||
import { Hooks } from '@poppinss/hooks' | ||
const hooks = new Hooks() | ||
hooks.add('before', 'save', onSave) | ||
hooks.add('creating', function (arg1, arg2, arg3) {}) | ||
hooks.add('creating', function (arg1, arg2, arg3) {}) | ||
hooks.add('creating', function (arg1, arg2, arg3) {}) | ||
// Later remove it | ||
hooks.remove('before', 'save', onSave) | ||
const runner = hooks.runner('creating') | ||
await runner.run('arg1', 'arg2', 'arg3') | ||
``` | ||
#### clear(lifecycle: 'before' | 'after', action?: string) | ||
It is usually helpful to inform the cleanup functions if there was an error or not. Maybe some cleanup functions may not to run only in case of errors. | ||
Clear all hooks for a given lifecycle and optionally an action. | ||
```ts | ||
hooks.clear('before') | ||
const hooks = new Hooks() | ||
// Clear just for the save action | ||
hooks.clear('before', 'save') | ||
``` | ||
hooks.add('creating', function (model) { | ||
const file = await saveFileToDisk() | ||
model.filePath = file | ||
#### merge (hooks: Hooks): void | ||
return (error, model) => { | ||
if (error) { | ||
await removeFileFromDisk(model.filePath) | ||
} | ||
} | ||
}) | ||
Merge hooks from an existing hooks instance. Useful during class inheritance. | ||
const runner = hooks.runner('creating') | ||
try { | ||
// Run hooks | ||
await runner.run(model) | ||
```ts | ||
const hooks = new Hooks() | ||
hooks.add('before', 'save', function () {}) | ||
// Run the actual action | ||
await model.save() | ||
} catch (error) { | ||
// During error | ||
await runner.cleaup(error, model) | ||
} | ||
const hooks1 = new Hooks() | ||
hooks1.merge(hooks) | ||
await hooks1.exec('before', 'save', []) | ||
/** | ||
* During success | ||
*/ | ||
if (runner.isCleanupPending) { | ||
await runner.cleaup(null, model) | ||
} | ||
``` | ||
@@ -137,0 +172,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
17624
0
18
11
266
183
1
1