├── .github └── workflows │ └── npm-publish.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── actions.ts ├── apphook.ts ├── commands.ts ├── hook_events.ts ├── index.ts ├── listeners.ts └── rules.ts ├── tests └── apphook.test.ts └── tsconfig.json /.github/workflows/npm-publish.yaml: -------------------------------------------------------------------------------- 1 | name: npm-publish 2 | on: push 3 | 4 | jobs: 5 | publish: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: actions/setup-node@v3 10 | with: 11 | node-version: 16.15.0 12 | - run: npm install 13 | - run: npm run test 14 | - run: npm run build 15 | - uses: JS-DevTools/npm-publish@v1 16 | with: 17 | token: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # TypeScript declarations 107 | types -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kelly Peilin Chan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![apphook](https://socialify.git.ci/apitable/apphook/image?description=1&font=Inter&language=1&name=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Dark) 3 | # apphook: Event-Manager Hook Engine 4 | 5 | [![npm](https://img.shields.io/npm/v/apphook)](https://www.npmjs.com/package/apphook) 6 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/apitable/apphook/npm-publish)](https://github.com/apitable/apphook/actions) 7 | [![npm bundle size](https://img.shields.io/bundlephobia/min/apphook)](https://www.npmjs.com/package/apphook) 8 | [![npm](https://img.shields.io/npm/dm/apphook)](https://www.npmjs.com/package/apphook) 9 | 10 | `apphook` is a way to implant/modify other piece of code. 11 | 12 | It is a lightweight Event-Manager that inspired by [Wordpress Hooks](https://developer.wordpress.org/plugins/hooks/) 13 | 14 | Computer software is a sort of sequence. We build software according to the business. 15 | 16 | However, the user's behaviors are out-of-sequence and chaos. 17 | 18 | You will never know what users want, how they use your software, what they want to customization. 19 | 20 | So, we need to prepare a system to deal with out-of-sequence and chaos, which can make big changes and flexible customization available. 21 | 22 | That's why we should know `AppHook`. 23 | 24 | `AppHook` is a hooks engine which Event-Driven. It can intercept users' behaviors and extend our functionalities. 25 | 26 | ## Quick Start 27 | 28 | ```bash 29 | npm i --save apphook 30 | ``` 31 | 32 | 33 | TypeScript types declarations (.d.ts) are ready. 34 | 35 | See [examples](#example) for usage examples. 36 | 37 | ## Principle 38 | 39 | We place AppHook hooks in code and trigger event. 40 | 41 | For example, when user click the button A, we can trigger a event called "click:button:A" 42 | 43 | We have two way to trigger event: 44 | 45 | 1. Trigger. When event appear, do some actions or behaviors, it would not change code pipeline path. 46 | 2. Filter. When event appear, it will do some actions and behaviors, return a object. It can be an interceptor. 47 | 48 | 49 | ## Use Case 50 | 51 | - Event Tracking:Don't need to hard code in the code anymore, we can put all event tracking code in the file by bind and unbind. 52 | - Rookie Onboarding: New register user onboarding 53 | - Help Guiding: when the 10th click on a button, popup a UI window. 54 | - Users Tasks: check whether user finished some tasks. 55 | - Marketing Events: If match some condition, do something like popup a marketing ui windows. 56 | - Users Recall: If user has not login 30 days, do something. 57 | - Payment Interception: When click a feature button, users have no payment yet, trigger and open a UI windows until payment finished and go on. 58 | - Third Party: customize 3rd plugins or more features 59 | - ...... 60 | 61 | ## Terms 62 | 63 | - hook: 64 | - hookState: 65 | - hookArgs: 66 | - binding: 67 | - add_trigger: 68 | - remove_trigger: 69 | - add_filter: 70 | - remove_filter: 71 | - trigger 72 | - triggerCommand: 73 | - triggerCommandArg :any 74 | - filter: 75 | - filterCommand: 76 | - filterCommandArg: 77 | - rule 78 | - condition 79 | - conditionArgs 80 | - action: 81 | - trigger action: 82 | - trigger command: 83 | - arg: 84 | - filter action: 85 | - filter command: 86 | - filter command arg: 87 | - listener : 88 | - trigger Listner: 89 | - filter Listner: 90 | 91 | 92 | 93 | 94 | ## Example 95 | 96 | ### Use Trigger to Event Tracking 97 | 98 | ```typescript 99 | // Window.tsx 100 | // ... 101 | onClickLoginButton: () => { 102 | // ... 103 | apphook.doTrigger('user:click_login_button'); 104 | 105 | } 106 | // ... 107 | ``` 108 | 109 | ```typescript 110 | // EventTracking.ts, a independent file for event tracking 111 | apphook.addTrigger('user:click_login_button', (args) => { 112 | 113 | // Event Tracking Code 114 | EventTracking.track('user:click_login_button', {...}); 115 | 116 | tracker.track('user:click_login_button', {...}); 117 | tracker.setProfile({email:'xxx@xxx.com'}); 118 | }); 119 | ``` 120 | 121 | 122 | ### Use Filter, make contact number nickname customizable 123 | 124 | ```typescript 125 | apphook.addFilter('get_form_name', (defaultValue, args) => { 126 | let user = args[0]; 127 | if (user.is_cloud) { 128 | return "Member ID"; 129 | } else if (user.is_self_hosted) { 130 | return "Employee ID"; 131 | } 132 | return defaultValue; 133 | }); 134 | ``` 135 | 136 | ```typescript 137 | // UI.tsx 138 |
139 | // Here will get the result "Member ID" or "Employee ID" or "ID" 140 | ``` 141 | 142 | ## Rookie popup guiding 143 | 144 | If you want: 145 | > When a female user get into your product the 10 times, popup "congratulation, you have used 10 times" 146 | 147 | Break it down: 148 | 149 | - trigger: user get into the 10th times 150 | - hook: get into product(application:start) 151 | - hookState: the 10th times 152 | - rule: female 153 | - condition: gender == femail 154 | - action: 155 | - command: popup 156 | - command: "congratulation, you have used 10 times" 157 | 158 | 159 | Relevant code: 160 | ```typescript 161 | // trigger event 162 | apphook.doTrigger('application:start', [], 10) // the 10th times get in 163 | 164 | // add trigger 165 | apphook.addTrigger('application:start', (args, hookState) => { 166 | if (hookState == 10) { 167 | showWindow('congratulation, you have used 10 times'); 168 | } 169 | }, { 170 | doCheck: (args) => { 171 | return user.gender === 'female'; 172 | }}); 173 | ``` 174 | 175 | 176 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security issues 2 | 3 | If you think you have found a security vulnerability, please send a report to [security@apitable.com](mailto:security@apitable.com). 4 | 5 | This address can be used for all of APITable Team's open source and commercial products (including but not limited to APITable, APITable.com, APITable Enterprise). 6 | 7 | We can accept only vulnerability reports at this address. 8 | 9 | APITable Team will send you a response indicating the next steps in handling your report. 10 | 11 | After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. 12 | 13 | **Important:** We ask you to not disclose the vulnerability before it have been fixed and announced, unless you received a response from the APITable security team that you can do so. 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apphook", 3 | "version": "1.0.7", 4 | "description": "A way to implant/modify other piece of code", 5 | "main": "./dist/index.js", 6 | "types": "./types/index.d.ts", 7 | "files": ["./dist", "./types"], 8 | "scripts": { 9 | "build": "tsc", 10 | "test": "jest --coverage tests" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/apitable/apphook.git" 15 | }, 16 | "keywords": [ 17 | "hook", 18 | "plugin", 19 | "wordpress", 20 | "event", 21 | "event-manager" 22 | ], 23 | "author": "Kelly Peilin Chan ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/apitable/apphook/issues" 27 | }, 28 | "homepage": "https://github.com/apitable/apphook", 29 | "devDependencies": { 30 | "@types/jest": "^29.2.0", 31 | "jest": "^29.2.1", 32 | "ts-jest": "^29.0.3", 33 | "typescript": "^4.8.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/actions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Action, behavior for apphook 3 | * Action = Command + Command Arguments 4 | * 5 | * @Author: Kelly Peilin Chan (kelly@apitable.com) 6 | * @Date: 2020-03-07 11:09:17 7 | * @Last Modified by: Kelly Peilin Chan (kelly@apitable.com) 8 | * @Last Modified time: 2020-03-10 14:03:19 9 | */ 10 | import { FilterCommand, TriggerCommand } from './commands'; 11 | 12 | export interface ITriggerAction { 13 | command: TriggerCommand; 14 | args: any[]; 15 | } 16 | export interface IFilterAction { 17 | command: FilterCommand; 18 | args: any[]; 19 | } 20 | -------------------------------------------------------------------------------- /src/apphook.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * AppHook Hook Engine 3 | * 4 | * @Author: Kelly Peilin Chan (kelly@apitable.com) 5 | * @Date: 2020-03-07 11:15:05 6 | * @Last Modified by: Kelly Peilin Chan (kelly@apitable.com) 7 | * @Last Modified time: 2020-03-21 18:25:48 8 | */ 9 | import { FilterCommand, TriggerCommand } from './commands'; 10 | import { IRule } from './rules'; 11 | import { IFilterAction, ITriggerAction } from './actions'; 12 | import { IListener, ITrigger, ListenerType as ListenerType, IFilter } from './listeners'; 13 | import { AddTriggerEvent, DoTriggerEvent, WhenApplyFiltersEvent, AddFilterEvent } from './hook_events'; 14 | 15 | interface IListenersMap { 16 | [listenerType: string]: { 17 | [hook: string]: IListener[]; 18 | }; 19 | } 20 | 21 | export class AppHook { 22 | 23 | /** 24 | * the place to store all listeners, including Trigger and Filter, store them by type 25 | * 26 | * @type {IListenersMap} 27 | * @memberof AppHook 28 | */ 29 | _listeners: IListenersMap = {}; 30 | 31 | /** 32 | * When AddTrigger triggered, the inner event. 33 | * 34 | * @type {AddTriggerEvent} 35 | * @memberof AppHook 36 | */ 37 | _onAddTrigger: AddTriggerEvent | undefined; 38 | 39 | /** 40 | * When DoTrigger triggered, the inner event. 41 | * 42 | * @type {DoTriggerEvent} 43 | * @memberof AppHook 44 | */ 45 | _onDoTrigger: DoTriggerEvent | undefined; 46 | 47 | /** 48 | * Use for inner self-inspection, 49 | * the event action that can be bound to useFilters 50 | * 51 | * @type {WhenApplyFiltersEvent} 52 | * @memberof AppHook 53 | */ 54 | _whenApplyFilters: WhenApplyFiltersEvent | undefined; 55 | 56 | /** 57 | * When AddFilter triggered, the inner event. 58 | * 59 | * @type {AddFilterEvent} 60 | * @memberof AppHook 61 | */ 62 | _onAddFilter: AddFilterEvent | undefined; 63 | 64 | // async applyFiltersAsync(hook: string, defaultValue: any, hookState?: any): Promise { 65 | 66 | // return await ; 67 | // } 68 | 69 | /** 70 | * 71 | * Add Trigger 72 | * 73 | * @param {string} hook 74 | * @param {TriggerCommand} command 75 | * @param {*} commandArg 76 | * @param {(IRule | undefined)} rule 77 | * @param {number} [priority=0] 78 | * @param {boolean} [isCatch=false] 79 | * @returns {ITrigger} 80 | * @memberof AppHook 81 | */ 82 | addTrigger(hook: string, 83 | command: TriggerCommand, 84 | commandArg: any, 85 | rule: IRule | undefined, 86 | priority = 0, 87 | isCatch = false): ITrigger { 88 | 89 | if (this._onAddTrigger != null) { 90 | this._onAddTrigger(hook, command, commandArg, rule, priority, isCatch); 91 | } 92 | 93 | const action: ITriggerAction = { 94 | command, 95 | args: commandArg, 96 | }; 97 | 98 | const trigger: ITrigger = { 99 | type: ListenerType.Filter, 100 | priority, 101 | hook, 102 | action, 103 | rule, 104 | isCatch, 105 | }; 106 | 107 | this.addListener(ListenerType.Trigger, hook, trigger); 108 | return trigger; 109 | } 110 | 111 | /** 112 | * Bind AddTriggerEvent, the method will be called when addTrigger 113 | * 114 | * @memberof AppHook 115 | */ 116 | bindAddTrigger(bind: AddTriggerEvent) { 117 | this._onAddTrigger = bind; 118 | } 119 | /** 120 | * 121 | * bind AddFilterEvent, the method will be called when addFilter 122 | * 123 | * @param {AddFilterEvent} bind 124 | * @memberof AppHook 125 | */ 126 | bindAddFilter(bind: AddFilterEvent) { 127 | this._onAddFilter = bind; 128 | } 129 | 130 | /** 131 | * Bind DoTriggerEvent, the method will be called when doTrigger 132 | * 133 | * @param {DoTriggerEvent} bind 134 | * @memberof AppHook 135 | */ 136 | bindDoTrigger(bind: DoTriggerEvent) { 137 | this._onDoTrigger = bind; 138 | } 139 | 140 | /** 141 | * binding the method will be called when applyFilters 142 | * 143 | * @param {WhenApplyFiltersEvent} bind 144 | * @memberof AppHook 145 | */ 146 | bindUseFilters(bind: WhenApplyFiltersEvent) { 147 | this._whenApplyFilters = bind; 148 | } 149 | 150 | /** 151 | * 152 | * Add filter, filter the default value when event is fired 153 | * 154 | * @param {string} hook 155 | * @param {FilterCommand} command 156 | * @param {*} commandArg 157 | * @param {(IRule | undefined)} rule 158 | * @param {number} [priority=0] 159 | * @param {boolean} [isCatch=false] 160 | * @returns {IFilter} 161 | * @memberof AppHook 162 | */ 163 | addFilter(hook: string, 164 | command: FilterCommand, 165 | commandArg: any, 166 | rule: IRule | undefined, 167 | priority = 0, 168 | isCatch = false): IFilter { 169 | 170 | if (this._onAddFilter != null) { 171 | this._onAddFilter(hook, command, commandArg, rule, priority, isCatch); 172 | } 173 | 174 | const action: IFilterAction = { 175 | command, 176 | args: commandArg, 177 | }; 178 | 179 | const filter: IFilter = { 180 | type: ListenerType.Filter, 181 | priority, 182 | hook, 183 | action, 184 | rule, 185 | isCatch, 186 | }; 187 | 188 | this.addListener(ListenerType.Filter, hook, filter); 189 | 190 | return filter; 191 | } 192 | 193 | /** 194 | * add listener to the listeners map 195 | * 196 | * @private 197 | * @param {('Trigger' | 'Filter')} type 198 | * @param {string} hook hook name 199 | * @param {IListener} newListener 200 | * @memberof AppHook 201 | */ 202 | private addListener(type: ListenerType, hook: string, newListener: IListener) { 203 | // listeners of the type 204 | let typeListners = this._listeners[type]; 205 | if (typeListners === undefined) { 206 | typeListners = this._listeners[type] = {}; 207 | } 208 | 209 | let listenerList = typeListners[hook]; 210 | if (listenerList === undefined) { 211 | listenerList = typeListners[hook] = [newListener]; 212 | } else { 213 | // order insert, the smaller the number, the more small index. 214 | for (let i = 0; i <= listenerList.length; i++) { 215 | const loopListener = listenerList[i]; 216 | 217 | if (i === listenerList.length) { // number biggest 218 | listenerList.push(newListener); 219 | break; 220 | } 221 | 222 | if (newListener.priority <= loopListener.priority) { 223 | listenerList.splice(i, 0, newListener); 224 | break; 225 | } 226 | } 227 | } 228 | } 229 | 230 | /** 231 | * remove listener 232 | * 233 | * @private 234 | * @param {ListenerType} type 235 | * @param {IListener} listener 236 | * @returns {boolean} 237 | * @memberof AppHook 238 | */ 239 | private removeListener(type: ListenerType, listener: IListener): boolean { 240 | const typeListners = this._listeners[type]; 241 | if (typeListners === undefined) { 242 | return false; 243 | } 244 | 245 | const listenerList = typeListners[listener.hook]; 246 | if (listenerList === undefined) { 247 | return false; 248 | } 249 | for (let i = 0; i < listenerList.length; i++) { 250 | const l = listenerList[i]; 251 | if (l === listener) { 252 | listenerList.splice(i, 1); 253 | return true; 254 | } 255 | } 256 | 257 | return false; 258 | } 259 | 260 | /** 261 | * active the trigger, map the trigger to the command 262 | * no need to consider the priority, because the priority is considered when add trigger 263 | * 264 | * @param {string} hook 265 | * @param {object} [hookState={}] hook event state, optional arguments 266 | * @memberof AppHook 267 | */ 268 | doTriggers(hook: string, hookState?: any) { 269 | if (this._onDoTrigger != null) { 270 | this._onDoTrigger(hook, hookState); 271 | } 272 | const triggerMap = this._listeners[ListenerType.Trigger]; 273 | if (triggerMap === undefined) { 274 | return; 275 | } 276 | const triggerList = triggerMap[hook]; 277 | if (triggerList === undefined) { 278 | return; 279 | } 280 | for (let i = 0; i < triggerList.length; i++) { 281 | const trigger = triggerList[i] as ITrigger; 282 | if (trigger.isCatch === undefined || trigger.isCatch === false) { 283 | trigger.action.command(hookState, trigger.action.args); 284 | } else { 285 | try { 286 | trigger.action.command(hookState, trigger.action.args); 287 | } catch (e) { 288 | console.error(e); 289 | } 290 | } 291 | } 292 | } 293 | 294 | /** 295 | * 296 | * remove triggers 297 | * 298 | * attention: pass the correct trigger reference(ref func pointer) 299 | * no deep comparison here 300 | * 301 | * @param {ITrigger} trigger 302 | * @returns {boolean} 303 | * @memberof AppHook 304 | */ 305 | removeTrigger(trigger: ITrigger): boolean { 306 | return this.removeListener(ListenerType.Trigger, trigger); 307 | } 308 | 309 | /** 310 | * whether has any specified hook name trigger 311 | * 312 | * @param {string} hook 313 | * @returns {boolean} 314 | * @memberof AppHook 315 | */ 316 | hasAnyTriggers(hook: string): boolean { 317 | return this.hasAnyListeners(ListenerType.Trigger, hook); 318 | } 319 | 320 | /** 321 | * whether has any specified hook name filter 322 | * 323 | * @param {string} hook 324 | * @returns {boolean} 325 | * @memberof AppHook 326 | */ 327 | hasAnyFilters(hook: string): boolean { 328 | return this.hasAnyListeners(ListenerType.Filter, hook); 329 | } 330 | 331 | /** 332 | * whether has any specified hook name listener(filter or trigger) 333 | * 334 | * @private 335 | * @param {ListenerType} type 336 | * @param {string} hook 337 | * @returns {boolean} 338 | * @memberof AppHook 339 | */ 340 | private hasAnyListeners(type: ListenerType, hook: string): boolean { 341 | const typeListeners = this._listeners[type]; 342 | if (typeListeners === undefined) { 343 | return false; 344 | } 345 | const hookListeners = typeListeners[hook]; 346 | if (hookListeners === undefined) { 347 | return false; 348 | } 349 | if (hookListeners.length === 0) { 350 | return false; 351 | } 352 | return true; 353 | } 354 | 355 | /** 356 | * remove filter 357 | * 358 | * attention: pass the correct trigger reference(ref func pointer) 359 | * no deep comparison here 360 | * 361 | * @param {IFilter} filter 362 | * @returns {boolean} 363 | * @memberof AppHook 364 | */ 365 | removeFilter(filter: IFilter): boolean { 366 | return this.removeListener(ListenerType.Filter, filter); 367 | } 368 | 369 | /** 370 | * apply filters, trigger the event, and implement multiple filters on the original string 371 | * 372 | * @param {string} hook 373 | * @param {*} defaultValue 374 | * @param {*} [hookState] 375 | * @returns {*} 376 | * @memberof AppHook 377 | */ 378 | applyFilters(hook: string, defaultValue: any, hookState?: any): any { 379 | if (this._whenApplyFilters != null) { 380 | this._whenApplyFilters(hook, defaultValue, hookState); 381 | } 382 | 383 | const filterMap = this._listeners[ListenerType.Filter]; 384 | if (filterMap === undefined) { 385 | return defaultValue; 386 | } 387 | const filterList = filterMap[hook]; 388 | if (filterList === undefined) { 389 | return defaultValue; 390 | } 391 | let filteredValue = defaultValue; 392 | for (let i = 0; i < filterList.length; i++) { 393 | const filter = filterList[i] as IFilter; 394 | if (filter.isCatch === undefined || filter.isCatch === false) { 395 | filteredValue = filter.action.command(filteredValue, hookState, filter.action.args); 396 | } else { 397 | try { 398 | filteredValue = filter.action.command(filteredValue, hookState, filter.action.args); 399 | } catch (e) { 400 | console.error(e); 401 | } 402 | } 403 | } 404 | return filteredValue; 405 | } 406 | 407 | } 408 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | * Trigger is a function that is directly executed 4 | * Filter has a return value and a default value parameter processing 5 | * 6 | * @Author: Kelly Peilin Chan (kelly@apitable.com) 7 | * @Date: 2020-03-09 19:43:51 8 | * @Last Modified by: Kelly Peilin Chan (kelly@apitable.com) 9 | * @Last Modified time: 2020-03-09 19:44:24 10 | */ 11 | 12 | export type TriggerCommand = (hookState: any, args: any[]) => void; 13 | 14 | export type FilterCommand = (defaultValue: any, hookState: any, args: any[]) => any; 15 | -------------------------------------------------------------------------------- /src/hook_events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * appHook inside events set 3 | * 4 | * @Author: Kelly Peilin Chan (kelly@apitable.com) 5 | * @Date: 2020-03-11 19:38:00 6 | * @Last Modified by: Kelly Peilin Chan (kelly@apitable.com) 7 | * @Last Modified time: 2020-03-19 14:22:02 8 | */ 9 | import { FilterCommand, TriggerCommand } from './commands'; 10 | import { IRule } from './rules'; 11 | 12 | export type AddTriggerEvent = (hook: string, 13 | command: TriggerCommand, 14 | commandArg: any, 15 | rule: IRule | undefined, 16 | priority: number, 17 | isCatch: boolean) => void; 18 | 19 | export type AddFilterEvent = (hook: string, 20 | command: FilterCommand, 21 | commandArg: any, 22 | rule: IRule | undefined, 23 | priority: number, 24 | isCatch: boolean) => void; 25 | 26 | export type DoTriggerEvent = (hook: string, hookState?: any) => void; 27 | 28 | /** 29 | * the event that will be triggered when use applyFilters 30 | */ 31 | export type WhenApplyFiltersEvent = (hook: string, defaultValue: any, hookState?: any) => void; 32 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './apphook'; 3 | export * from './listeners'; 4 | export * from './rules'; 5 | export * from './commands'; 6 | export * from './actions'; 7 | -------------------------------------------------------------------------------- /src/listeners.ts: -------------------------------------------------------------------------------- 1 | import { IRule } from './rules'; 2 | import { ITriggerAction, IFilterAction } from './actions'; 3 | 4 | /** 5 | * Listener interface 6 | * 7 | * @export 8 | * @interface IListener 9 | */ 10 | export interface IListener { 11 | type: ListenerType; 12 | priority: number; 13 | /** 14 | * hook name 15 | */ 16 | hook: string; 17 | rule?: IRule; 18 | isCatch?: boolean; 19 | } 20 | 21 | export interface ITrigger extends IListener { 22 | action: ITriggerAction; 23 | } 24 | 25 | export interface IFilter extends IListener { 26 | action: IFilterAction; 27 | } 28 | 29 | export enum ListenerType { 30 | Trigger = 'Trigger', 31 | Filter = 'Filter', 32 | } 33 | -------------------------------------------------------------------------------- /src/rules.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IRule { 3 | condition: ICondition; 4 | args: any[]; 5 | } 6 | 7 | export interface ICondition { 8 | doCheck(): boolean; 9 | } 10 | -------------------------------------------------------------------------------- /tests/apphook.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * AppHook Module Unit Tests 3 | * 4 | * @Author: Kelly Peilin Chan (kelly@apitable.com) 5 | * @Date: 2020-03-09 19:46:02 6 | * @Last Modified by: Kelly Peilin Chan (kelly@apitable.com) 7 | * @Last Modified time: 2022-10-19 14:26:00 8 | */ 9 | import { IFilter, ICondition, IRule, ITrigger, AppHook } from '../src'; 10 | 11 | class TestCondition implements ICondition { 12 | doCheck(): boolean { 13 | return true; 14 | } 15 | } 16 | 17 | describe('test appHook', () => { 18 | it('should call trigger event ok', () => { 19 | const apphook = new AppHook(); 20 | 21 | expect(apphook.hasAnyTriggers('test_trigger_event')).toBe(false); 22 | 23 | const rule: IRule = { 24 | condition: new TestCondition(), 25 | args: [], 26 | }; 27 | 28 | let triggerResult = false; 29 | 30 | const trigger: ITrigger = apphook.addTrigger( 31 | 'test_trigger_event', 32 | (hookState, _args) => { 33 | expect(hookState).toBe(123); 34 | triggerResult = true; 35 | }, 36 | [], 37 | rule); 38 | 39 | expect(apphook.hasAnyTriggers('test_trigger_event')).toBe(true); 40 | 41 | apphook.doTriggers('test_trigger_event', 123); 42 | 43 | expect(triggerResult.toString()).toBe('true'); 44 | expect(trigger.hook).toBe('test_trigger_event'); 45 | 46 | // test catch erro (red error) 47 | // apphook.addTrigger( 48 | // 'test_error_event', (state, _args) => { 49 | // expect(state).toBe(321); 50 | // throw new Error('TestError'); 51 | // }, 52 | // [], 53 | // rule, 54 | // 0, 55 | // true); 56 | // apphook.doTriggers('test_error_event', 321); 57 | }); 58 | // it('should call trigger event ok (async)', async () => { 59 | // const apphook = new AppHook(); 60 | // async filter 61 | // const filter1: IFilter = apphook.addFilterAsync('test', 62 | // defaultValue => (defaultValue + ' Filtered1'), 63 | // []); 64 | // expect(filter1.hook).toBe('test'); 65 | // async action 66 | 67 | // }); 68 | 69 | it('should call filter event ok', () => { 70 | const apphook = new AppHook(); 71 | 72 | expect(apphook.hasAnyFilters('get_test_name')).toBe(false); 73 | 74 | const rule: IRule = { 75 | condition: { 76 | doCheck: () => true, 77 | }, 78 | args: [], 79 | }; 80 | 81 | // Single layer filter 82 | const filter1: IFilter = apphook.addFilter('get_test_name', 83 | defaultValue => (defaultValue + ' Filtered1'), 84 | [], 85 | rule); 86 | expect(filter1.hook).toBe('get_test_name'); 87 | 88 | const filterd1 = apphook.applyFilters('get_test_name', 'Test Name'); 89 | expect(filterd1).toBe('Test Name Filtered1'); 90 | 91 | // Double layer filter 92 | const filter2: IFilter = apphook.addFilter('get_test_name', 93 | defaultValue => (defaultValue + ' Filtered2'), 94 | [], 95 | rule); 96 | expect(filter2.hook).toBe('get_test_name'); 97 | 98 | const filterd2 = apphook.applyFilters('get_test_name', 'Test Name'); 99 | expect(filterd2).toBe('Test Name Filtered2 Filtered1'); 100 | 101 | // third layer filter 102 | const filter3: IFilter = apphook.addFilter('get_test_name', 103 | defaultValue => (defaultValue + ' Filtered3'), 104 | [], 105 | rule, 999); 106 | expect(filter3.hook).toBe('get_test_name'); 107 | 108 | const filterd3 = apphook.applyFilters('get_test_name', 'Test Name'); 109 | expect(filterd3).toBe('Test Name Filtered2 Filtered1 Filtered3'); 110 | 111 | // delete filter 112 | apphook.removeFilter(filter1); 113 | 114 | const filterd4 = apphook.applyFilters('get_test_name', 'Test Name'); 115 | expect(filterd4).toBe('Test Name Filtered2 Filtered3'); 116 | 117 | expect(apphook.hasAnyFilters('get_test_name')).toBe(true); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "outDir":"dist", 10 | "sourceMap": true, 11 | "declaration": true, 12 | "declarationDir": "./types" 13 | }, 14 | "include": ["src/**/*"], 15 | "$schema": "https://json.schemastore.org/tsconfig", 16 | "display": "Recommended" 17 | } --------------------------------------------------------------------------------