This commit is contained in:
Tutur33
2023-11-24 22:35:41 +01:00
parent 3c0b507a93
commit 7644b2a0f7
45165 changed files with 4803356 additions and 3 deletions
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2021 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+147
View File
@@ -0,0 +1,147 @@
<div align="center"><img src="https://res.cloudinary.com/adonisjs/image/upload/q_100/v1557762307/poppinss_iftxlt.jpg" width="600px"></div>
# Hooks
> A no brainer module to execute lifecycle hooks in sequence.
[![gh-workflow-image]][gh-workflow-url] [![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url] [![synk-image]][synk-url]
I find myself re-writing the code for hooks in multiple packages, so decided to extract it to it's own module, that can be re-used by other modules of AdonisJS.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of contents
- [How it works?](#how-it-works)
- [Installation](#installation)
- [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)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## How it works?
The hooks class exposes the API to `register`, `remove` and `exec` lifecycle hooks for any number of actions or events. The class API is meant to be used internally and not by the user facing code and this gives you the chance to improve the hooks DX.
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.
> Also generics increases the number of types Typescript has to generate and it's better to avoid them whenever possible.
## Installation
Install the package from npm registry as follows:
```sh
npm i @poppinss/hooks
# yarn
yarn add @poppinss/hooks
```
## Usage
Use it as follows
```ts
import { Hooks } from '@poppinss/hooks'
const hooks = new Hooks()
hooks.add('before', 'save', function () {})
// Later invoke before save hooks
await hooks.exec('before', 'save', { id: 1 })
```
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.
```ts
import { Ioc } from '@adonisjs/fold'
const ioc = new Ioc()
const resolver = ioc.getResolver(undefined, 'modelHooks', 'App/Models/Hooks')
const hooks = new Hooks(resolver)
```
The resolver allows the end user to pass the hook reference as string and hooks must live inside `App/Models/Hooks` folder.
```ts
hooks.add('before', 'save', 'User.encryptPassword')
```
## API
#### add(lifecycle: 'before' | 'after', action: string, handler: Function | string)
Add a new hook handler.
```ts
hooks.add('before', 'save', (data) => {
console.log(data)
})
```
#### exec(lifecycle: 'before' | 'after', action: string, ...data: any[])
Execute a given hook for a selected lifecycle.
```ts
hooks.exec('before', 'save', { username: 'virk' })
```
#### remove (lifecycle: 'before' | 'after', action: string, handler: HooksHandler | string)
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() {}
hooks.add('before', 'save', onSave)
// Later remove it
hooks.remove('before', 'save', onSave)
```
#### clear(lifecycle: 'before' | 'after', action?: string)
Clear all hooks for a given lifecycle and optionally an action.
```ts
hooks.clear('before')
// Clear just for the save action
hooks.clear('before', 'save')
```
#### merge (hooks: Hooks): void
Merge hooks from an existing hooks instance. Useful during class inheritance.
```ts
const hooks = new Hooks()
hooks.add('before', 'save', function () {})
const hooks1 = new Hooks()
hooks1.merge(hooks)
await hooks1.exec('before', 'save', [])
```
[gh-workflow-image]: https://img.shields.io/github/workflow/status/poppinss/hooks/test?style=for-the-badge
[gh-workflow-url]: https://github.com/poppinss/hooks/actions/workflows/test.yml "Github action"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
[npm-image]: https://img.shields.io/npm/v/@poppinss/hooks.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@poppinss/hooks 'npm'
[license-image]: https://img.shields.io/npm/l/@poppinss/hooks?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md 'license'
[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/poppinss/hooks?label=Synk%20Vulnerabilities&style=for-the-badge
[synk-url]: https://snyk.io/test/github/poppinss/hooks?targetFile=package.json 'synk'
@@ -0,0 +1 @@
export { Hooks } from './src/Hooks';
@@ -0,0 +1,13 @@
"use strict";
/*
* @poppinss/hooks
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Hooks = void 0;
var Hooks_1 = require("./src/Hooks");
Object.defineProperty(exports, "Hooks", { enumerable: true, get: function () { return Hooks_1.Hooks; } });
@@ -0,0 +1,59 @@
/// <reference types="@adonisjs/application/build/adonis-typings/application" />
declare type HooksHandler = (...args: any[]) => void | Promise<void>;
/**
* Exposes the API to register before/after lifecycle hooks for a given action
* with option to resolve handlers from the IoC container.
*
* 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.
*/
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
*/
private ensureResolver;
/**
* Resolves the hook handler using the resolver when it is defined as string
* or returns the function reference back
*/
private resolveHandler;
/**
* Returns handlers set for a given action or undefined
*/
private getActionHandlers;
/**
* Adds the resolved handler to the actions set
*/
private addResolvedHandler;
/**
* Returns a boolean whether a handler has been already registered or not
*/
has(lifecycle: 'before' | 'after', action: string, handler: HooksHandler | string): boolean;
/**
* 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 all handlers for a given action or lifecycle. If action is not
* defined, then all actions for that given lifecycle are removed
*/
clear(lifecycle: 'before' | 'after', 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(hooks: Hooks): void;
/**
* Executes the hook handler for a given action and lifecycle
*/
exec(lifecycle: 'before' | 'after', action: string, ...data: any[]): Promise<void>;
}
export {};
@@ -0,0 +1,137 @@
"use strict";
/*
* @poppinss/hooks
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Hooks = void 0;
/**
* Exposes the API to register before/after lifecycle hooks for a given action
* with option to resolve handlers from the IoC container.
*
* 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.
*/
class Hooks {
constructor(resolver) {
this.resolver = resolver;
this.hooks = {
before: new Map(),
after: new Map(),
};
}
/**
* Raise exceptins when resolver is not defined
*/
ensureResolver() {
if (!this.resolver) {
throw new Error('IoC container resolver is required to register string based hooks handlers');
}
}
/**
* Resolves the hook handler using the resolver when it is defined as string
* or returns the function reference back
*/
resolveHandler(handler) {
if (typeof handler === 'string') {
this.ensureResolver();
return this.resolver.resolve(handler);
}
return handler;
}
/**
* Returns handlers set for a given action or undefined
*/
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);
if (!handlers) {
return false;
}
return handlers.has(this.resolveHandler(handler));
}
/**
* Register hook handler for a given event and lifecycle
*/
add(lifecycle, action, handler) {
this.addResolvedHandler(lifecycle, action, this.resolveHandler(handler));
return this;
}
/**
* Remove a pre-registered handler
*/
remove(lifecycle, action, handler) {
const handlers = this.getActionHandlers(lifecycle, action);
if (!handlers) {
return;
}
handlers.delete(this.resolveHandler(handler));
}
/**
* Remove all handlers for a given action or lifecycle. If action is not
* defined, then all actions for that given lifecycle are removed
*/
clear(lifecycle, action) {
if (!action) {
this.hooks[lifecycle].clear();
return;
}
this.hooks[lifecycle].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(hooks) {
hooks.hooks.before.forEach((actionHooks, action) => {
actionHooks.forEach((handler) => {
this.addResolvedHandler('before', 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
*/
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);
}
}
}
}
exports.Hooks = Hooks;
+134
View File
@@ -0,0 +1,134 @@
{
"name": "@poppinss/hooks",
"version": "5.0.3",
"description": "A no brainer hooks module for execute before/after lifecycle hooks",
"main": "build/index.js",
"files": [
"build/src",
"build/index.d.ts",
"build/index.js"
],
"scripts": {
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"test": "node .bin/test.js",
"clean": "del build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"format": "prettier --write .",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json poppinss/hooks"
},
"peerDependencies": {
"@adonisjs/application": ">=4.0.0"
},
"peerDependenciesMeta": {
"@adonisjs/application": {
"optional": true
}
},
"devDependencies": {
"@adonisjs/application": "^5.2.0",
"@adonisjs/mrm-preset": "^5.0.3",
"@adonisjs/require-ts": "^2.0.11",
"@types/node": "^17.0.23",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^4.0.1",
"doctoc": "^2.0.1",
"eslint": "^8.12.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-adonis": "^2.1.0",
"eslint-plugin-prettier": "^4.0.0",
"github-label-sync": "^2.2.0",
"husky": "^7.0.1",
"japa": "^4.0.0",
"mrm": "^4.0.0",
"np": "^7.6.1",
"prettier": "^2.6.2",
"typescript": "^4.6.3"
},
"repository": {
"type": "git",
"url": "git+https://github.com/poppinss/hooks.git"
},
"keywords": [
"hooks",
"poppinss"
],
"author": "virk,poppinss",
"license": "MIT",
"bugs": {
"url": "https://github.com/poppinss/hooks/issues"
},
"homepage": "https://github.com/poppinss/hooks#readme",
"nyc": {
"exclude": [
"test"
],
"extension": [
".ts"
]
},
"husky": {
"hooks": {
"commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js"
}
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"np": {
"contents": ".",
"anyBranch": false
},
"mrmConfig": {
"core": false,
"license": "MIT",
"services": [
"github-actions"
],
"minNodeVersion": "16.13.1",
"probotApps": [
"stale",
"lock"
],
"runGhActionsOnWindows": false
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
}
}