├── .github
├── SECURITY.md
└── workflows
│ ├── release.yml
│ ├── test.yml
│ └── update-prettier.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── examples
├── authentication-example.md
├── store-example.md
└── test-mock-example.md
├── index.d.ts
├── index.js
├── index.test-d.ts
├── jsconfig.json
├── lib
├── add.js
├── register.js
└── remove.js
├── package-lock.json
├── package.json
└── test
├── after.test.js
├── before.test.js
├── constructor.test.js
├── error.test.js
├── hook.test.js
├── remove.test.js
├── singular-hook.test.js
├── testrunner.js
├── unit-register.test.js
├── unit-remove.test.js
└── wrap.test.js
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | ## Security contact information
2 |
3 | To report a security vulnerability, please use the
4 | [Tidelift security contact](https://tidelift.com/security).
5 | Tidelift will coordinate the fix and disclosure.
6 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | "on":
3 | push:
4 | branches:
5 | - "*.x"
6 | - main
7 | - next
8 | - beta
9 | jobs:
10 | release:
11 | name: release
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: actions/setup-node@v3
16 | with:
17 | node-version: lts/*
18 | cache: npm
19 | - run: npm ci
20 | - run: npx semantic-release
21 | env:
22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | types: [opened, synchronize]
8 |
9 | jobs:
10 | test-deno:
11 | runs-on: ubuntu-latest
12 | needs: test-node
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: denoland/setup-deno@v2
16 | with:
17 | deno-version: v2.x # Run with latest stable Deno.
18 | - run: deno install
19 | - run: deno test --no-check
20 |
21 | test-bun:
22 | runs-on: ubuntu-latest
23 | needs: test-node
24 | steps:
25 | - uses: actions/checkout@v4
26 | - uses: oven-sh/setup-bun@v2
27 | - run: bun install
28 | - run: bun test
29 |
30 | test-node:
31 | runs-on: ubuntu-latest
32 | strategy:
33 | matrix:
34 | node_version:
35 | - 20
36 | - 22
37 | - 24
38 |
39 | steps:
40 | - uses: actions/checkout@v3
41 | - name: Use Node.js ${{ matrix.node_version }}
42 | uses: actions/setup-node@v3
43 | with:
44 | node-version: ${{ matrix.node_version }}
45 | cache: npm
46 | - run: npm ci
47 | - run: npm run test:code
48 |
49 | test:
50 | runs-on: ubuntu-latest
51 | needs:
52 | - test-bun
53 | - test-deno
54 | - test-node
55 | # prevent to be taken as pass when `test_matrix` was not picked up yet
56 | if: ${{ always() }}
57 | steps:
58 | - run: exit 1
59 | if: ${{ needs.test-bun.result != 'success' || needs.test-deno.result != 'success' || needs.test-node.result != 'success' }}
60 | - uses: actions/checkout@v3
61 | - uses: actions/setup-node@v3
62 | with:
63 | node-version: 24
64 | cache: npm
65 | - run: npm ci
66 | - run: npm run test:tsc
67 | - run: npm run test:tsd
68 | - run: npm run lint
69 |
--------------------------------------------------------------------------------
/.github/workflows/update-prettier.yml:
--------------------------------------------------------------------------------
1 | name: Update Prettier
2 | "on":
3 | push:
4 | branches:
5 | - renovate/prettier-*
6 | workflow_dispatch: {}
7 | jobs:
8 | update_prettier:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | - uses: actions/setup-node@v3
13 | with:
14 | cache: npm
15 | node-version: lts/*
16 | - run: npm ci
17 | - run: npm run lint:fix
18 | - run: |
19 | git config user.name github-actions
20 | git config user.email github-actions@github.com
21 | git add .
22 | git commit -m "style: prettier" && git push || true
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at opensource+coc@martynus.net. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: https://contributor-covenant.org
74 | [version]: https://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2018 Gregor Martynus and other contributors.
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # before-after-hook
2 |
3 | > asynchronous hooks for internal functionality
4 |
5 | [](https://www.npmjs.com/package/before-after-hook)
6 | [](https://github.com/gr2m/before-after-hook/actions/workflows/test.yml)
7 |
8 | ## Usage
9 |
10 |
11 |
12 |
13 | Browsers
14 | |
15 | Load before-after-hook directly from cdn.skypack.dev
16 |
17 | ```html
18 |
21 | ```
22 |
23 | |
24 |
25 | Node
26 | |
27 |
28 | Install with npm install before-after-hook
29 |
30 | ```js
31 | import Hook from "before-after-hook";
32 | ```
33 |
34 | |
35 |
36 |
37 |
38 | ### Singular hook
39 |
40 | ```js
41 | // instantiate singular hook API
42 | const hook = new Hook.Singular();
43 |
44 | // Create a hook
45 | async function getData(options) {
46 | try {
47 | const result = await hook(fetchFromDatabase, options);
48 | return handleData(result);
49 | } catch (error) {
50 | return handleGetError(error);
51 | }
52 | }
53 |
54 | // register before/error/after hooks.
55 | // The methods can be async or return a promise
56 | hook.before(beforeHook);
57 | hook.error(errorHook);
58 | hook.after(afterHook);
59 |
60 | getData({ id: 123 });
61 | ```
62 |
63 | ### Hook collection
64 |
65 | ```js
66 | // instantiate hook collection API
67 | const hookCollection = new Hook.Collection();
68 |
69 | // Create a hook
70 | async function getData(options) {
71 | try {
72 | const result = await hookCollection("get", fetchFromDatabase, options);
73 | return handleData(result);
74 | } catch (error) {
75 | return handleGetError(error);
76 | }
77 | }
78 |
79 | // register before/error/after hooks.
80 | // The methods can be async or return a promise
81 | hookCollection.before("get", beforeHook);
82 | hookCollection.error("get", errorHook);
83 | hookCollection.after("get", afterHook);
84 |
85 | getData({ id: 123 });
86 | ```
87 |
88 | ### Hook.Singular vs Hook.Collection
89 |
90 | There's no fundamental difference between the `Hook.Singular` and `Hook.Collection` hooks except for the fact that a hook from a collection requires you to pass along the name. Therefore the following explanation applies to both code snippets as described above.
91 |
92 | The methods are executed in the following order
93 |
94 | 1. `beforeHook`
95 | 2. `fetchFromDatabase`
96 | 3. `afterHook`
97 | 4. `handleData`
98 |
99 | `beforeHook` can mutate `options` before it’s passed to `fetchFromDatabase`.
100 |
101 | If an error is thrown in `beforeHook` or `fetchFromDatabase` then `errorHook` is
102 | called next.
103 |
104 | If `afterHook` throws an error then `handleGetError` is called instead
105 | of `handleData`.
106 |
107 | If `errorHook` throws an error then `handleGetError` is called next, otherwise
108 | `afterHook` and `handleData`.
109 |
110 | You can also use `hook.wrap` to achieve the same thing as shown above (collection example):
111 |
112 | ```js
113 | hookCollection.wrap("get", async (getData, options) => {
114 | await beforeHook(options);
115 |
116 | try {
117 | const result = getData(options);
118 | } catch (error) {
119 | await errorHook(error, options);
120 | }
121 |
122 | await afterHook(result, options);
123 | });
124 | ```
125 |
126 | ## API
127 |
128 | - [Singular Hook Constructor](#singular-hook-api)
129 | - [Hook Collection Constructor](#hook-collection-api)
130 |
131 | ## Singular hook API
132 |
133 | - [Singular constructor](#singular-constructor)
134 | - [hook.api](#singular-api)
135 | - [hook()](#singular-api)
136 | - [hook.before()](#singular-api)
137 | - [hook.error()](#singular-api)
138 | - [hook.after()](#singular-api)
139 | - [hook.wrap()](#singular-api)
140 | - [hook.remove()](#singular-api)
141 |
142 | ### Singular constructor
143 |
144 | The `Hook.Singular` constructor has no options and returns a `hook` instance with the
145 | methods below:
146 |
147 | ```js
148 | const hook = new Hook.Singular();
149 | ```
150 |
151 | Using the singular hook is recommended for [TypeScript](#typescript)
152 |
153 | ### Singular API
154 |
155 | The singular hook is a reference to a single hook. This means that there's no need to pass along any identifier (such as a `name` as can be seen in the [Hook.Collection API](#hookcollectionapi)).
156 |
157 | The API of a singular hook is exactly the same as a collection hook and we therefore suggest you read the [Hook.Collection API](#hookcollectionapi) and leave out any use of the `name` argument. Just skip it like described in this example:
158 |
159 | ```js
160 | const hook = new Hook.Singular();
161 |
162 | // good
163 | hook.before(beforeHook);
164 | hook.after(afterHook);
165 | hook(fetchFromDatabase, options);
166 |
167 | // bad
168 | hook.before("get", beforeHook);
169 | hook.after("get", afterHook);
170 | hook("get", fetchFromDatabase, options);
171 | ```
172 |
173 | ## Hook collection API
174 |
175 | - [Collection constructor](#collection-constructor)
176 | - [hookCollection.api](#hookcollectionapi)
177 | - [hookCollection()](#hookcollection)
178 | - [hookCollection.before()](#hookcollectionbefore)
179 | - [hookCollection.error()](#hookcollectionerror)
180 | - [hookCollection.after()](#hookcollectionafter)
181 | - [hookCollection.wrap()](#hookcollectionwrap)
182 | - [hookCollection.remove()](#hookcollectionremove)
183 |
184 | ### Collection constructor
185 |
186 | The `Hook.Collection` constructor has no options and returns a `hookCollection` instance with the
187 | methods below
188 |
189 | ```js
190 | const hookCollection = new Hook.Collection();
191 | ```
192 |
193 | ### hookCollection.api
194 |
195 | Use the `api` property to return the public API:
196 |
197 | - [hookCollection.before()](#hookcollectionbefore)
198 | - [hookCollection.after()](#hookcollectionafter)
199 | - [hookCollection.error()](#hookcollectionerror)
200 | - [hookCollection.wrap()](#hookcollectionwrap)
201 | - [hookCollection.remove()](#hookcollectionremove)
202 |
203 | That way you don’t need to expose the [hookCollection()](#hookcollection) method to consumers of your library
204 |
205 | ### hookCollection()
206 |
207 | Invoke before and after hooks. Returns a promise.
208 |
209 | ```js
210 | hookCollection(nameOrNames, method /*, options */);
211 | ```
212 |
213 |
214 |
215 |
216 | Argument |
217 | Type |
218 | Description |
219 | Required |
220 |
221 |
222 |
223 | name |
224 | String or Array of Strings |
225 | Hook name, for example 'save' . Or an array of names, see example below. |
226 | Yes |
227 |
228 |
229 | method |
230 | Function |
231 | Callback to be executed after all before hooks finished execution successfully. options is passed as first argument |
232 | Yes |
233 |
234 |
235 | options |
236 | Object |
237 | Will be passed to all before hooks as reference, so they can mutate it |
238 | No, defaults to empty object ({} ) |
239 |
240 |
241 |
242 | Resolves with whatever `method` returns or resolves with.
243 | Rejects with error that is thrown or rejected with by
244 |
245 | 1. Any of the before hooks, whichever rejects / throws first
246 | 2. `method`
247 | 3. Any of the after hooks, whichever rejects / throws first
248 |
249 | Simple Example
250 |
251 | ```js
252 | hookCollection(
253 | "save",
254 | (record) => {
255 | return store.save(record);
256 | },
257 | record
258 | );
259 | // shorter: hookCollection('save', store.save, record)
260 |
261 | hookCollection.before("save", function addTimestamps(record) {
262 | const now = new Date().toISOString();
263 | if (record.createdAt) {
264 | record.updatedAt = now;
265 | } else {
266 | record.createdAt = now;
267 | }
268 | });
269 | ```
270 |
271 | Example defining multiple hooks at once.
272 |
273 | ```js
274 | hookCollection(
275 | ["add", "save"],
276 | (record) => {
277 | return store.save(record);
278 | },
279 | record
280 | );
281 |
282 | hookCollection.before("add", function addTimestamps(record) {
283 | if (!record.type) {
284 | throw new Error("type property is required");
285 | }
286 | });
287 |
288 | hookCollection.before("save", function addTimestamps(record) {
289 | if (!record.type) {
290 | throw new Error("type property is required");
291 | }
292 | });
293 | ```
294 |
295 | Defining multiple hooks is helpful if you have similar methods for which you want to define separate hooks, but also an additional hook that gets called for all at once. The example above is equal to this:
296 |
297 | ```js
298 | hookCollection(
299 | "add",
300 | (record) => {
301 | return hookCollection(
302 | "save",
303 | (record) => {
304 | return store.save(record);
305 | },
306 | record
307 | );
308 | },
309 | record
310 | );
311 | ```
312 |
313 | ### hookCollection.before()
314 |
315 | Add before hook for given name.
316 |
317 | ```js
318 | hookCollection.before(name, method);
319 | ```
320 |
321 |
322 |
323 |
324 | Argument |
325 | Type |
326 | Description |
327 | Required |
328 |
329 |
330 |
331 | name |
332 | String |
333 | Hook name, for example 'save' |
334 | Yes |
335 |
336 |
337 | method |
338 | Function |
339 |
340 | Executed before the wrapped method. Called with the hook’s
341 | options argument. Before hooks can mutate the passed options
342 | before they are passed to the wrapped method.
343 | |
344 | Yes |
345 |
346 |
347 |
348 | Example
349 |
350 | ```js
351 | hookCollection.before("save", function validate(record) {
352 | if (!record.name) {
353 | throw new Error("name property is required");
354 | }
355 | });
356 | ```
357 |
358 | ### hookCollection.error()
359 |
360 | Add error hook for given name.
361 |
362 | ```js
363 | hookCollection.error(name, method);
364 | ```
365 |
366 |
367 |
368 |
369 | Argument |
370 | Type |
371 | Description |
372 | Required |
373 |
374 |
375 |
376 | name |
377 | String |
378 | Hook name, for example 'save' |
379 | Yes |
380 |
381 |
382 | method |
383 | Function |
384 |
385 | Executed when an error occurred in either the wrapped method or a
386 | before hook. Called with the thrown error
387 | and the hook’s options argument. The first method
388 | which does not throw an error will set the result that the after hook
389 | methods will receive.
390 | |
391 | Yes |
392 |
393 |
394 |
395 | Example
396 |
397 | ```js
398 | hookCollection.error("save", (error, options) => {
399 | if (error.ignore) return;
400 | throw error;
401 | });
402 | ```
403 |
404 | ### hookCollection.after()
405 |
406 | Add after hook for given name.
407 |
408 | ```js
409 | hookCollection.after(name, method);
410 | ```
411 |
412 |
413 |
414 |
415 | Argument |
416 | Type |
417 | Description |
418 | Required |
419 |
420 |
421 |
422 | name |
423 | String |
424 | Hook name, for example 'save' |
425 | Yes |
426 |
427 |
428 | method |
429 | Function |
430 |
431 | Executed after wrapped method. Called with what the wrapped method
432 | resolves with the hook’s options argument.
433 | |
434 | Yes |
435 |
436 |
437 |
438 | Example
439 |
440 | ```js
441 | hookCollection.after("save", (result, options) => {
442 | if (result.updatedAt) {
443 | app.emit("update", result);
444 | } else {
445 | app.emit("create", result);
446 | }
447 | });
448 | ```
449 |
450 | ### hookCollection.wrap()
451 |
452 | Add wrap hook for given name.
453 |
454 | ```js
455 | hookCollection.wrap(name, method);
456 | ```
457 |
458 |
459 |
460 |
461 | Argument |
462 | Type |
463 | Description |
464 | Required |
465 |
466 |
467 |
468 | name |
469 | String |
470 | Hook name, for example 'save' |
471 | Yes |
472 |
473 |
474 | method |
475 | Function |
476 |
477 | Receives both the wrapped method and the passed options as arguments so it can add logic before and after the wrapped method, it can handle errors and even replace the wrapped method altogether
478 | |
479 | Yes |
480 |
481 |
482 |
483 | Example
484 |
485 | ```js
486 | hookCollection.wrap("save", async (saveInDatabase, options) => {
487 | if (!record.name) {
488 | throw new Error("name property is required");
489 | }
490 |
491 | try {
492 | const result = await saveInDatabase(options);
493 |
494 | if (result.updatedAt) {
495 | app.emit("update", result);
496 | } else {
497 | app.emit("create", result);
498 | }
499 |
500 | return result;
501 | } catch (error) {
502 | if (error.ignore) return;
503 | throw error;
504 | }
505 | });
506 | ```
507 |
508 | See also: [Test mock example](examples/test-mock-example.md)
509 |
510 | ### hookCollection.remove()
511 |
512 | Removes hook for given name.
513 |
514 | ```js
515 | hookCollection.remove(name, hookMethod);
516 | ```
517 |
518 |
519 |
520 |
521 | Argument |
522 | Type |
523 | Description |
524 | Required |
525 |
526 |
527 |
528 | name |
529 | String |
530 | Hook name, for example 'save' |
531 | Yes |
532 |
533 |
534 | beforeHookMethod |
535 | Function |
536 |
537 | Same function that was previously passed to hookCollection.before() , hookCollection.error() , hookCollection.after() or hookCollection.wrap()
538 | |
539 | Yes |
540 |
541 |
542 |
543 | Example
544 |
545 | ```js
546 | hookCollection.remove("save", validateRecord);
547 | ```
548 |
549 | ## TypeScript
550 |
551 | This library contains type definitions for TypeScript.
552 |
553 | ### Type support for `Singular`:
554 |
555 | ```ts
556 | import Hook from "before-after-hook";
557 |
558 | type TOptions = { foo: string }; // type for options
559 | type TResult = { bar: number }; // type for result
560 | type TError = Error; // type for error
561 |
562 | const hook = new Hook.Singular();
563 |
564 | hook.before((options) => {
565 | // `options.foo` has `string` type
566 |
567 | // not allowed
568 | options.foo = 42;
569 |
570 | // allowed
571 | options.foo = "Forty-Two";
572 | });
573 |
574 | const hookedMethod = hook(
575 | (options) => {
576 | // `options.foo` has `string` type
577 |
578 | // not allowed, because it does not satisfy the `R` type
579 | return { foo: 42 };
580 |
581 | // allowed
582 | return { bar: 42 };
583 | },
584 | { foo: "Forty-Two" }
585 | );
586 | ```
587 |
588 | You can choose not to pass the types for options, result or error. So, these are completely valid:
589 |
590 | ```ts
591 | const hook = new Hook.Singular();
592 | const hook = new Hook.Singular();
593 | const hook = new Hook.Singular();
594 | ```
595 |
596 | In these cases, the omitted types will implicitly be `any`.
597 |
598 | ### Type support for `Collection`:
599 |
600 | `Collection` also has strict type support. You can use it like this:
601 |
602 | ```ts
603 | import { Hook } from "before-after-hook";
604 |
605 | type HooksType = {
606 | add: {
607 | Options: { type: string };
608 | Result: { id: number };
609 | Error: Error;
610 | };
611 | save: {
612 | Options: { type: string };
613 | Result: { id: number };
614 | };
615 | read: {
616 | Options: { id: number; foo: number };
617 | };
618 | destroy: {
619 | Options: { id: number; foo: string };
620 | };
621 | };
622 |
623 | const hooks = new Hook.Collection();
624 |
625 | hooks.before("destroy", (options) => {
626 | // `options.id` has `number` type
627 | });
628 |
629 | hooks.error("add", (err, options) => {
630 | // `options.type` has `string` type
631 | // `err` is `instanceof Error`
632 | });
633 |
634 | hooks.error("save", (err, options) => {
635 | // `options.type` has `string` type
636 | // `err` has type `any`
637 | });
638 |
639 | hooks.after("save", (result, options) => {
640 | // `options.type` has `string` type
641 | // `result.id` has `number` type
642 | });
643 | ```
644 |
645 | You can choose not to pass the types altogether. In that case, everything will implicitly be `any`:
646 |
647 | ```ts
648 | const hook = new Hook.Collection();
649 | ```
650 |
651 | Alternative imports:
652 |
653 | ```ts
654 | import { Singular, Collection } from "before-after-hook";
655 |
656 | const hook = new Singular();
657 | const hooks = new Collection();
658 | ```
659 |
660 | ## Upgrading to 1.4
661 |
662 | Since version 1.4 the `Hook` constructor has been deprecated in favor of returning `Hook.Singular` in an upcoming breaking release.
663 |
664 | Version 1.4 is still 100% backwards-compatible, but if you want to continue using hook collections, we recommend using the `Hook.Collection` constructor instead before the next release.
665 |
666 | For even more details, check out [the PR](https://github.com/gr2m/before-after-hook/pull/52).
667 |
668 | ## See also
669 |
670 | If `before-after-hook` is not for you, have a look at one of these alternatives:
671 |
672 | - https://github.com/keystonejs/grappling-hook
673 | - https://github.com/sebelga/promised-hooks
674 | - https://github.com/bnoguchi/hooks-js
675 | - https://github.com/cb1kenobi/hook-emitter
676 |
677 | ## License
678 |
679 | [Apache 2.0](LICENSE)
680 |
--------------------------------------------------------------------------------
/examples/authentication-example.md:
--------------------------------------------------------------------------------
1 | # Authentication hook examples
2 |
3 | This example shows how an imaginary `authentication` API with methods to manage user accounts and sessions can expose hook APIs to implement
4 |
5 | - username validation
6 | - an email verification flow
7 |
8 | The authentication APIs to create a user account and a session look as follows
9 |
10 | ```js
11 | authentication.accounts.add(properties);
12 | authentication.sessions.add(credentials);
13 | ```
14 |
15 | The exposed hooks could be
16 |
17 | - `registration`
18 | invoked when a user account gets created
19 | - `login`
20 | invoked when a user tries to sign in
21 |
22 | The implementation of the hooks could look like this:
23 |
24 | ```js
25 | const hook = new Hook.Collection();
26 |
27 | function createUserAccount (properties) {
28 | return hook('registration', properties, (properties) => (
29 | return accountDatabase.create(properties)
30 | ))
31 | }
32 | function createSession (account, credentials) {
33 | const options = {account: account, credentials: credentials}
34 | return hook('login', options, (options) => {
35 | if (!validateCredentials(options.account, options.credentials) {
36 | throw new Error('Unauthorized: username or password is incorrect')
37 | })
38 |
39 | return {
40 | id: generateSessionId(options.account, options.credentials, secret),
41 | account: options.account
42 | }
43 | })
44 | }
45 | ```
46 |
47 | ## Validate username
48 |
49 | Say we want to enforce that username must be valid email addresses.
50 |
51 | ```js
52 | hook.before("registration", (properties) => {
53 | if (!isValidEmail(properties.username)) {
54 | throw new Error(properties.username + "is not a valid email address");
55 | }
56 |
57 | properties.username = properties.username.toLowerCase();
58 | });
59 | ```
60 |
61 | ## Implement email verification flow
62 |
63 | Say we do not want to allow users to sign in to their account until their email was verified. Once verified, we set a `verifiedAt` timestamp.
64 |
65 | ```js
66 | hook.before("login", (options) => {
67 | if (!options.account.verifiedAt) {
68 | throw new Error("You must verify your email address before signing in");
69 | }
70 | });
71 | ```
72 |
--------------------------------------------------------------------------------
/examples/store-example.md:
--------------------------------------------------------------------------------
1 | # Store hook examples
2 |
3 | This example shows how an imaginary `store` API with methods to add, update
4 | and remove methods can expose a hook API to intercept all of them.
5 |
6 | We will use the hooks to
7 |
8 | - add validation
9 | - add timestamps and user references
10 | - emit events
11 |
12 | The store APIs to change data look as follows, we ignore methods to find / query
13 | data for the sake of simplicity
14 |
15 | ```js
16 | store.add(document);
17 | store.update(id, changedProperties);
18 | store.remove(id);
19 | ```
20 |
21 | The exposed hooks could be
22 |
23 | - `add`
24 | invoked when a document gets added
25 | - `update`
26 | invoked when a document gets updated
27 | - `remove`
28 | invoked when a document is removed
29 | - `save`
30 | invoked each time a document gets saved within the hooks listed above.
31 |
32 | The implementation of the hooks could look like this:
33 |
34 | ```js
35 | const hook = new Hook.Collection();
36 |
37 | function storeAdd(newDoc) {
38 | return hook(["add", "save"], newDoc, (newDoc) => {
39 | return database.create(newDoc);
40 | });
41 | }
42 | ```
43 |
44 | This will invoke the following
45 |
46 | 1. `add` before hook
47 | 1. `save` before hook
48 | 1. run `database.create(newDoc)`
49 | 1. `save` after hook
50 | 1. `add` after hook
51 |
52 | ## Validation
53 |
54 | We want to build a todo application which has documents with `type` set to
55 | `item` and `list`. We don’t want to allow any other values for `type` or
56 | documents with a `type` property altogether. We also want to make sure that
57 | each `item` has a `listId` property
58 |
59 | ```js
60 | hook.before("save", (doc) => {
61 | if (!doc.type) {
62 | throw new Error("type property is required");
63 | }
64 | if (doc.type !== "item" && doc.type !== "list") {
65 | throw new Error(
66 | "Invalid type value: " + doc.type + " (Allowed: item, list)"
67 | );
68 | }
69 |
70 | if (doc.type === "item" && !doc.listId) {
71 | throw new Error("items need to set listId property");
72 | }
73 |
74 | return doc;
75 | });
76 | ```
77 |
78 | This will prevent the app from saving invalid documents
79 |
80 | ```js
81 | store.add({ foo: "bar" });
82 | // rejects with Error: type property is required
83 | ```
84 |
85 | ```js
86 | store.add({ type: "item", listId: "id34567", note: "Remember the milk!" });
87 | // resolves with value of database.create(newDoc)
88 | ```
89 |
90 | ## timestamps and user reference
91 |
92 | ```js
93 | hook.before("add", (doc) => {
94 | doc.createdAt = new Date().toISOString();
95 | doc.createdBy = app.currentUser.id;
96 | return doc;
97 | });
98 | hook.before("update", (doc) => {
99 | doc.updatedAt = new Date().toISOString();
100 | doc.updatedBy = app.currentUser.id;
101 | return doc;
102 | });
103 | ```
104 |
105 | ## events
106 |
107 | ```js
108 | hook.after("add", (doc) => {
109 | app.emit("data:" + doc.type + ":add", doc);
110 | });
111 | hook.after("update", (doc) => {
112 | app.emit("data:" + doc.type + ":update", doc);
113 | });
114 | hook.after("remove", (doc) => {
115 | app.emit("data:" + doc.type + ":remove", doc);
116 | });
117 | ```
118 |
--------------------------------------------------------------------------------
/examples/test-mock-example.md:
--------------------------------------------------------------------------------
1 | # Test mock example
2 |
3 | This example shows how a REST API client can implement mocking for simple testing.
4 |
5 | Let’s say the client has methods to get/list/create/update/delete todos.
6 |
7 | ```js
8 | client.getTodo(id);
9 | client.listTodos();
10 | client.createTodo(options);
11 | client.updateTodo(options);
12 | client.deleteTodo(id);
13 | ```
14 |
15 | The exposed hook is called `request`.
16 |
17 | All methods would use an internal `request` method to which they pass options
18 | such as the http verb, the URL, query parameters and request body.
19 |
20 | The implementation of the hook could look like this:
21 |
22 | ```js
23 | function request (options) {
24 | return hook('request', options, (options) => (
25 | const {url, ...fetchOptions} = options
26 | return fetch(url, fetchOptions)
27 | ))
28 | }
29 | ```
30 |
31 | Now when writing a test for "getTodo", the wrap hook allows us to prevent from
32 | an HTTP request to be even sent, instead we test if the options passed `fetch`
33 | are what we expect.
34 |
35 | ```js
36 | test("client.getTodo(123)", () => {
37 | const client = getClient();
38 | client.hook.wrap("request", (fetch, options) => {
39 | assert.equal(options.method, "GET");
40 | assert.equal(options.url, "https://api.acme-inc.com/todos/123");
41 | });
42 |
43 | client.getTodo(123);
44 | });
45 | ```
46 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | type HookMethod = (
2 | options: Options
3 | ) => Result | Promise;
4 |
5 | type BeforeHook = (options: Options) => void | Promise;
6 | type ErrorHook = (
7 | error: Error,
8 | options: Options
9 | ) => unknown | Promise;
10 | type AfterHook = (
11 | result: Result,
12 | options: Options
13 | ) => void | Promise;
14 | type WrapHook = (
15 | hookMethod: HookMethod,
16 | options: Options
17 | ) => Result | Promise;
18 |
19 | type AnyHook =
20 | | BeforeHook
21 | | ErrorHook
22 | | AfterHook
23 | | WrapHook;
24 |
25 | type TypeStoreKey = "Options" | "Result" | "Error";
26 | type TypeStore = { [key in TypeStoreKey]?: any };
27 | type GetType<
28 | TStore extends TypeStore,
29 | TKey extends TypeStoreKey
30 | > = TKey extends keyof TStore ? TStore[TKey] : any;
31 |
32 | export interface HookCollection<
33 | HooksType extends Record = Record<
34 | string,
35 | { Options: any; Result: any; Error: any }
36 | >,
37 | HookName extends keyof HooksType = keyof HooksType
38 | > {
39 | /**
40 | * Invoke before and after hooks
41 | */
42 | (
43 | name: Name | Name[],
44 | hookMethod: HookMethod<
45 | GetType,
46 | GetType
47 | >,
48 | options?: GetType
49 | ): Promise>;
50 | /**
51 | * Add `before` hook for given `name`
52 | */
53 | before(
54 | name: Name,
55 | beforeHook: BeforeHook>
56 | ): void;
57 | /**
58 | * Add `error` hook for given `name`
59 | */
60 | error(
61 | name: Name,
62 | errorHook: ErrorHook<
63 | GetType,
64 | GetType
65 | >
66 | ): void;
67 | /**
68 | * Add `after` hook for given `name`
69 | */
70 | after(
71 | name: Name,
72 | afterHook: AfterHook<
73 | GetType,
74 | GetType
75 | >
76 | ): void;
77 | /**
78 | * Add `wrap` hook for given `name`
79 | */
80 | wrap(
81 | name: Name,
82 | wrapHook: WrapHook<
83 | GetType,
84 | GetType
85 | >
86 | ): void;
87 | /**
88 | * Remove added hook for given `name`
89 | */
90 | remove(
91 | name: Name,
92 | hook: AnyHook<
93 | GetType,
94 | GetType,
95 | GetType
96 | >
97 | ): void;
98 | /**
99 | * Public API
100 | */
101 | api: Pick<
102 | HookCollection,
103 | "before" | "error" | "after" | "wrap" | "remove"
104 | >;
105 | }
106 |
107 | export interface HookSingular {
108 | /**
109 | * Invoke before and after hooks
110 | */
111 | (hookMethod: HookMethod, options?: Options): Promise;
112 | /**
113 | * Add `before` hook
114 | */
115 | before(beforeHook: BeforeHook): void;
116 | /**
117 | * Add `error` hook
118 | */
119 | error(errorHook: ErrorHook): void;
120 | /**
121 | * Add `after` hook
122 | */
123 | after(afterHook: AfterHook): void;
124 | /**
125 | * Add `wrap` hook
126 | */
127 | wrap(wrapHook: WrapHook): void;
128 | /**
129 | * Remove added hook
130 | */
131 | remove(hook: AnyHook): void;
132 | /**
133 | * Public API
134 | */
135 | api: Pick<
136 | HookSingular,
137 | "before" | "error" | "after" | "wrap" | "remove"
138 | >;
139 | }
140 |
141 | type Collection = new <
142 | HooksType extends Record = Record
143 | >() => HookCollection;
144 | type Singular = new <
145 | Options = unknown,
146 | Result = unknown,
147 | Error = unknown
148 | >() => HookSingular;
149 |
150 | interface Hook {
151 | /**
152 | * Creates a collection of hooks
153 | */
154 | Collection: Collection;
155 |
156 | /**
157 | * Creates a nameless hook that supports strict typings
158 | */
159 | Singular: Singular;
160 | }
161 |
162 | declare const Hook: {
163 | /**
164 | * Creates a collection of hooks
165 | */
166 | Collection: Collection;
167 |
168 | /**
169 | * Creates a nameless hook that supports strict typings
170 | */
171 | Singular: Singular;
172 | };
173 |
174 | export default Hook;
175 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { register } from "./lib/register.js";
4 | import { addHook } from "./lib/add.js";
5 | import { removeHook } from "./lib/remove.js";
6 |
7 | // bind with array of arguments: https://stackoverflow.com/a/21792913
8 | const bind = Function.bind;
9 | const bindable = bind.bind(bind);
10 |
11 | function bindApi(hook, state, name) {
12 | const removeHookRef = bindable(removeHook, null).apply(
13 | null,
14 | name ? [state, name] : [state]
15 | );
16 | hook.api = { remove: removeHookRef };
17 | hook.remove = removeHookRef;
18 | ["before", "error", "after", "wrap"].forEach((kind) => {
19 | const args = name ? [state, kind, name] : [state, kind];
20 | hook[kind] = hook.api[kind] = bindable(addHook, null).apply(null, args);
21 | });
22 | }
23 |
24 | function Singular() {
25 | const singularHookName = Symbol("Singular");
26 | const singularHookState = {
27 | registry: {},
28 | };
29 | const singularHook = register.bind(null, singularHookState, singularHookName);
30 | bindApi(singularHook, singularHookState, singularHookName);
31 | return singularHook;
32 | }
33 |
34 | function Collection() {
35 | const state = {
36 | registry: {},
37 | };
38 |
39 | const hook = register.bind(null, state);
40 | bindApi(hook, state);
41 |
42 | return hook;
43 | }
44 |
45 | export default { Singular, Collection };
46 |
--------------------------------------------------------------------------------
/index.test-d.ts:
--------------------------------------------------------------------------------
1 | import { expectType } from "tsd";
2 | import Hook from "./index.js";
3 |
4 | export async function singularReadmeTest() {
5 | // instantiate singular hook API
6 | const hook = new Hook.Singular();
7 |
8 | expectType>(hook);
9 |
10 | // Create a hook
11 | const method = () => {};
12 | function getData(options: unknown) {
13 | return hook(method, options);
14 | }
15 |
16 | // register before/error/after hooks.
17 | // The methods can be async or return a promise
18 | hook.before(() => {});
19 | hook.error(() => {});
20 | hook.after(() => {});
21 |
22 | expectType>(getData({ id: 123 }));
23 | }
24 |
25 | export function collectionReadmeTest() {
26 | // instantiate hook collection API
27 | const hookCollection = new Hook.Collection();
28 |
29 | expectType>(hookCollection);
30 |
31 | // Create a hook
32 | const method = () => {};
33 | function getData(options: unknown) {
34 | return hookCollection("get", method, options);
35 | }
36 |
37 | // register before/error/after hooks.
38 | // The methods can be async or return a promise
39 | hookCollection.before("get", () => {});
40 | hookCollection.error("get", () => {});
41 | hookCollection.after("get", () => {});
42 |
43 | expectType>(getData({ id: 123 }));
44 | }
45 |
46 | // TODO: add assertions below
47 |
48 | type Options = {
49 | [key: string]: any;
50 | everything: number;
51 | };
52 |
53 | const options: Options = {
54 | everything: 42,
55 | nothing: 0,
56 | };
57 |
58 | const hookMethod = (options: Options): number => {
59 | const sumOfNumbers = Object.keys(options)
60 | .map((key) => options[key])
61 | .filter((v): v is number => typeof v === "number")
62 | .reduce((sum, num) => sum + num, 0);
63 |
64 | return sumOfNumbers;
65 | };
66 |
67 | const beforeHook = (_options: Options): void => {};
68 | const afterHook = (_result: number, _options: Options): void => {};
69 | const errorHook = (_error: any, _options: Options): void => {};
70 | const errorCatchHook = (_error: any, _options: Options) => "ok";
71 | const wrapHook = (hookMethod: any, options: Options): number => {
72 | beforeHook(options);
73 | const result = hookMethod(options);
74 | afterHook(result, options);
75 | return result;
76 | };
77 |
78 | const hooks = new Hook.Collection();
79 |
80 | hooks.before("life", beforeHook);
81 | hooks.after("life", afterHook);
82 | hooks.error("life", errorHook);
83 | hooks.error("life", errorCatchHook);
84 | hooks.wrap("life", wrapHook);
85 |
86 | hooks("life", hookMethod);
87 | hooks(["life"], hookMethod);
88 |
89 | const hook = new Hook.Singular();
90 |
91 | hook.before(beforeHook);
92 | hook.after(afterHook);
93 | hook.error(errorHook);
94 | hook.error(errorCatchHook);
95 | hook.wrap(wrapHook);
96 |
97 | hook(hookMethod, options);
98 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strictNullChecks": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/lib/add.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | export function addHook(state, kind, name, hook) {
4 | const orig = hook;
5 | if (!state.registry[name]) {
6 | state.registry[name] = [];
7 | }
8 |
9 | if (kind === "before") {
10 | hook = (method, options) => {
11 | return Promise.resolve()
12 | .then(orig.bind(null, options))
13 | .then(method.bind(null, options));
14 | };
15 | }
16 |
17 | if (kind === "after") {
18 | hook = (method, options) => {
19 | let result;
20 | return Promise.resolve()
21 | .then(method.bind(null, options))
22 | .then((result_) => {
23 | result = result_;
24 | return orig(result, options);
25 | })
26 | .then(() => {
27 | return result;
28 | });
29 | };
30 | }
31 |
32 | if (kind === "error") {
33 | hook = (method, options) => {
34 | return Promise.resolve()
35 | .then(method.bind(null, options))
36 | .catch((error) => {
37 | return orig(error, options);
38 | });
39 | };
40 | }
41 |
42 | state.registry[name].push({
43 | hook: hook,
44 | orig: orig,
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/lib/register.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | export function register(state, name, method, options) {
4 | if (typeof method !== "function") {
5 | throw new Error("method for before hook must be a function");
6 | }
7 |
8 | if (!options) {
9 | options = {};
10 | }
11 |
12 | if (Array.isArray(name)) {
13 | return name.reverse().reduce((callback, name) => {
14 | return register.bind(null, state, name, callback, options);
15 | }, method)();
16 | }
17 |
18 | return Promise.resolve().then(() => {
19 | if (!state.registry[name]) {
20 | return method(options);
21 | }
22 |
23 | return state.registry[name].reduce((method, registered) => {
24 | return registered.hook.bind(null, method, options);
25 | }, method)();
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/lib/remove.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | export function removeHook(state, name, method) {
4 | if (!state.registry[name]) {
5 | return;
6 | }
7 |
8 | const index = state.registry[name]
9 | .map((registered) => {
10 | return registered.orig;
11 | })
12 | .indexOf(method);
13 |
14 | if (index === -1) {
15 | return;
16 | }
17 |
18 | state.registry[name].splice(index, 1);
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "before-after-hook",
3 | "type": "module",
4 | "version": "0.0.0-development",
5 | "description": "asynchronous before/error/after hooks for internal functionality",
6 | "exports": "./index.js",
7 | "types": "./index.d.ts",
8 | "files": [
9 | "index.js",
10 | "index.d.ts",
11 | "lib"
12 | ],
13 | "scripts": {
14 | "test": "npm run test:code && npm run test:tsc && npm run test:tsd && npm run lint",
15 | "test:code": "c8 --100 --clean node --test",
16 | "test:deno": "deno test --no-check",
17 | "test:tsc": "tsc --allowJs --noEmit --esModuleInterop --skipLibCheck --lib es2020 index.js",
18 | "test:tsd": "tsd",
19 | "lint": "prettier --check \"*.{js,json,ts,md}\" \"test//*.{js,json,ts,md}\" \".github/**/*.yml\"",
20 | "lint:fix": "prettier --write \"*.{js,json,ts,md}\" \"test//*.{js,json,ts,md}\" \".github/**/*.yml\"",
21 | "postcoverage": "open-cli coverage/index.html"
22 | },
23 | "repository": "github:gr2m/before-after-hook",
24 | "keywords": [
25 | "hook",
26 | "hooks",
27 | "api"
28 | ],
29 | "author": "Gregor Martynus",
30 | "license": "Apache-2.0",
31 | "devDependencies": {
32 | "@types/node": "^22.15.17",
33 | "c8": "^10.1.3",
34 | "prettier": "^2.0.0",
35 | "tsd": "^0.24.1",
36 | "typescript": "^4.8.4"
37 | },
38 | "release": {
39 | "branches": [
40 | "+([0-9]).x",
41 | "main",
42 | "next",
43 | {
44 | "name": "beta",
45 | "prerelease": true
46 | }
47 | ]
48 | },
49 | "renovate": {
50 | "extends": [
51 | "github>gr2m/.github"
52 | ]
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/test/after.test.js:
--------------------------------------------------------------------------------
1 | import { test, assert } from "./testrunner.js";
2 |
3 | import Hook from "../index.js";
4 |
5 | test('hook.after("test", afterCheck) order', async () => {
6 | const hook = new Hook.Collection();
7 | const calls = [];
8 |
9 | hook.after("test", () => {
10 | calls.push("after");
11 | });
12 |
13 | await hook("test", () => {
14 | calls.push("afterCheck");
15 | });
16 |
17 | assert(calls.length === 2);
18 | assert(calls[0] === "afterCheck");
19 | assert(calls[1] === "after");
20 | });
21 |
22 | test('hook.after("test", afterCheck) async afterCheck', async () => {
23 | const hook = new Hook.Collection();
24 | const calls = [];
25 |
26 | hook.after("test", async () => {
27 | calls.push("after");
28 | });
29 |
30 | await hook("test", () => {
31 | calls.push("afterCheck");
32 | });
33 |
34 | assert(calls.length === 2);
35 | assert(calls[0] === "afterCheck");
36 | assert(calls[1] === "after");
37 | });
38 |
39 | test('hook.after("test", afterCheck) throws error', async () => {
40 | const hook = new Hook.Collection();
41 | const method = function method() {};
42 |
43 | hook.after("test", () => {
44 | throw new Error("oops");
45 | });
46 |
47 | try {
48 | await hook("test", method);
49 | assert(false, "must not resolve");
50 | } catch (error) {
51 | assert(
52 | error.message === "oops",
53 | "rejects with error message from afterCheck"
54 | );
55 | }
56 | });
57 |
58 | test('hook.after("test", afterCheck) rejected promise', async () => {
59 | const hook = new Hook.Collection();
60 | const method = function method() {};
61 |
62 | hook.after("test", () => {
63 | return Promise.reject(new Error("oops"));
64 | });
65 |
66 | try {
67 | await hook("test", method);
68 | assert(false, "must not resolve");
69 | } catch (error) {
70 | assert(
71 | error.message === "oops",
72 | "rejects with error message from afterCheck"
73 | );
74 | }
75 | });
76 |
77 | test('hook.after("test", afterCheck) result and options', async () => {
78 | const hook = new Hook.Collection();
79 |
80 | hook.after("test", (result, options) => {
81 | assert(options.optionFoo === "bar", "passes options to after hook");
82 | result.foo = "newbar";
83 | });
84 | hook.after("test", (result, options) => {
85 | result.baz = "ar";
86 | });
87 |
88 | const result = await hook(
89 | "test",
90 | () => {
91 | return {
92 | foo: "bar",
93 | otherFoo: "bar",
94 | };
95 | },
96 | { optionFoo: "bar" }
97 | );
98 |
99 | assert(result.foo === "newbar", "after hook modifies result");
100 | assert(result.otherFoo === "bar", "result is not modified by after hook");
101 | });
102 |
103 | test('hook.after("test", afterCheck) multiple after hooks get executed after method', async () => {
104 | const hook = new Hook.Collection();
105 | const calls = [];
106 |
107 | hook.after("test", () => {
108 | calls.push("after1");
109 | });
110 | hook.after("test", () => {
111 | calls.push("after2");
112 | });
113 |
114 | await hook("test", () => {
115 | calls.push("afterCheck");
116 | });
117 |
118 | assert(calls.length === 3);
119 | assert(calls[0] === "afterCheck");
120 | assert(calls[1] === "after1");
121 | assert(calls[2] === "after2");
122 | });
123 |
--------------------------------------------------------------------------------
/test/before.test.js:
--------------------------------------------------------------------------------
1 | import { test, assert } from "./testrunner.js";
2 |
3 | import Hook from "../index.js";
4 |
5 | test('hook.before("test", check) order', async () => {
6 | const hook = new Hook.Collection();
7 | const calls = [];
8 |
9 | hook.before("test", () => {
10 | calls.push("before");
11 | });
12 |
13 | await hook("test", () => {
14 | calls.push("check");
15 | });
16 |
17 | assert(calls.length === 2);
18 | assert(calls[0] === "before");
19 | assert(calls[1] === "check");
20 | });
21 |
22 | test('hook.before("test", check) async check', async () => {
23 | const hook = new Hook.Collection();
24 | const calls = [];
25 |
26 | hook.before("test", async () => {
27 | calls.push("before");
28 | });
29 |
30 | await hook("test", () => {
31 | calls.push("check");
32 | });
33 |
34 | assert(calls.length === 2);
35 | assert(calls[0] === "before");
36 | assert(calls[1] === "check");
37 | });
38 |
39 | test('hook.before("test", check) throws error', async () => {
40 | const hook = new Hook.Collection();
41 |
42 | let methodCallCount = 0;
43 | const method = function method() {
44 | methodCallCount++;
45 | };
46 |
47 | hook.before("test", () => {
48 | throw new Error("oops");
49 | });
50 |
51 | try {
52 | await hook("test", method);
53 | assert(false, "must not resolve");
54 | } catch (error) {
55 | assert(error.message === "oops", "rejects with error message from check");
56 | assert(methodCallCount === 0);
57 | }
58 | });
59 |
60 | test('hook.before("test", check) rejected promise', async () => {
61 | const hook = new Hook.Collection();
62 |
63 | let methodCallCount = 0;
64 | const method = function method() {
65 | methodCallCount++;
66 | };
67 |
68 | hook.before("test", () => {
69 | return Promise.reject(new Error("oops"));
70 | });
71 |
72 | try {
73 | await hook("test", method);
74 | assert(false, "must not resolve");
75 | } catch (error) {
76 | assert(error.message === "oops", "rejects with error message from check");
77 | assert(methodCallCount === 0);
78 | }
79 | });
80 |
81 | test('hook.before("test", check) options', async () => {
82 | const hook = new Hook.Collection();
83 |
84 | hook.before("test", (options) => {
85 | options.foo = "bar";
86 | });
87 | hook.before("test", (options) => {
88 | options.baz = "ar";
89 | });
90 |
91 | await hook(
92 | "test",
93 | (options) => {
94 | assert(options.foo === "bar", "passes options to before hook");
95 | assert(options.baz === "ar", "passes options to before hook");
96 | assert(options.otherbar === "baz", "passes options to before hook");
97 | },
98 | { foo: "notbar", otherbar: "baz" }
99 | );
100 | });
101 |
102 | test('before("test", check) multiple before hooks get executed before method', async () => {
103 | const hook = new Hook.Collection();
104 | const calls = [];
105 |
106 | hook.before("test", () => {
107 | calls.push("before1");
108 | });
109 | hook.before("test", () => {
110 | calls.push("before2");
111 | });
112 |
113 | await hook("test", () => {
114 | calls.push("check");
115 | });
116 |
117 | assert(calls.length === 3);
118 | assert(calls[0] === "before2");
119 | assert(calls[1] === "before1");
120 | assert(calls[2] === "check");
121 | });
122 |
--------------------------------------------------------------------------------
/test/constructor.test.js:
--------------------------------------------------------------------------------
1 | import { test, assert } from "./testrunner.js";
2 |
3 | import Hook from "../index.js";
4 |
5 | test("new Hook.Singular()", () => {
6 | const hook = new Hook.Singular();
7 |
8 | assert(typeof hook === "function", "hook() is a function");
9 | assert(typeof hook.before === "function", "hook.before() is function");
10 | assert(typeof hook.after === "function", "hook.after() is function");
11 |
12 | assert(typeof hook.api === "object", "hook.api is an object");
13 | assert(typeof hook.api.before === "function", "hook.before() is function");
14 | assert(typeof hook.api.after === "function", "hook.after() is function");
15 | assert(typeof hook.api === "object", "does ont expose hook() method");
16 | });
17 |
18 | test("new Hook.Collection()", () => {
19 | const hook = new Hook.Collection();
20 |
21 | assert(typeof hook, "function", "hook() is a function");
22 | assert(typeof hook.before === "function", "hook.before() is function");
23 | assert(typeof hook.after === "function", "hook.after() is function");
24 |
25 | assert(typeof hook.api === "object", "hook.api is an object");
26 | assert(typeof hook.api.before === "function", "hook.before() is function");
27 | assert(typeof hook.api.after === "function", "hook.after() is function");
28 | assert(typeof hook.api === "object", "does ont expose hook() method");
29 | });
30 |
--------------------------------------------------------------------------------
/test/error.test.js:
--------------------------------------------------------------------------------
1 | import { test, assert } from "./testrunner.js";
2 |
3 | import Hook from "../index.js";
4 |
5 | test('hook.error("test", handleError) order', async () => {
6 | const hook = new Hook.Collection();
7 | const calls = [];
8 |
9 | hook.error("test", () => {
10 | calls.push("errorHook");
11 | });
12 | hook.after("test", () => {
13 | calls.push("afterHook");
14 | });
15 |
16 | await hook("test", () => {
17 | calls.push("method");
18 | throw new Error("oops");
19 | });
20 |
21 | assert(calls.length === 3);
22 | assert(calls[0] === "method");
23 | assert(calls[1] === "errorHook");
24 | assert(calls[2] === "afterHook");
25 | });
26 |
27 | test('hook.error("test", handleError) async order', async () => {
28 | const hook = new Hook.Collection();
29 | const calls = [];
30 |
31 | hook.error("test", () => {
32 | calls.push("errorHook");
33 | });
34 |
35 | await hook("test", () => {
36 | return new Promise(() => {
37 | calls.push("method");
38 | throw new Error("oops");
39 | });
40 | });
41 |
42 | assert(calls.length === 2);
43 | assert(calls[0] === "method");
44 | assert(calls[1] === "errorHook");
45 | });
46 |
47 | test('hook.error("test", handleError) can mutate error', async () => {
48 | const hook = new Hook.Collection();
49 | const method = function method() {
50 | throw new Error("oops");
51 | };
52 |
53 | hook.error("test", (error) => {
54 | error.message = "error hook";
55 | throw error;
56 | });
57 |
58 | try {
59 | await hook("test", method);
60 | assert(false, "must not resolve");
61 | } catch (error) {
62 | assert(
63 | error.message === "error hook",
64 | "rejects with error message from error hook"
65 | );
66 | }
67 | });
68 |
69 | test('hook.error("test", handleError) rejected promise', async () => {
70 | const hook = new Hook.Collection();
71 | const method = function method() {
72 | throw new Error("oops");
73 | };
74 |
75 | hook.error("test", (error) => {
76 | error.message = "error hook";
77 | return Promise.reject(error);
78 | });
79 |
80 | try {
81 | await hook("test", method);
82 | assert(false, "must not resolve");
83 | } catch (error) {
84 | assert(
85 | error.message,
86 | "error hook",
87 | "rejects with error message from error hook"
88 | );
89 | }
90 | });
91 |
92 | test('hook.error("test", handleError) can catch error', async () => {
93 | const hook = new Hook.Collection();
94 | const method = function method() {
95 | throw new Error("oops");
96 | };
97 |
98 | hook.error("test", () => {
99 | return { ok: true };
100 | });
101 |
102 | const result = await hook("test", method);
103 |
104 | assert(result.ok === true);
105 | });
106 |
107 | test('hook.error("test", handleError) receives options', async () => {
108 | const hook = new Hook.Collection();
109 | const method = function method() {
110 | throw new Error("oops");
111 | };
112 |
113 | hook.error("test", (error, options) => {
114 | assert(options.optionFoo === "bar");
115 | throw error;
116 | });
117 |
118 | try {
119 | await hook("test", method, { optionFoo: "bar" });
120 | assert(false, "must not resolve");
121 | } catch (error) {
122 | assert(error.message === "oops");
123 | }
124 | });
125 |
126 | test('hook.error("test", handleError) multiple error hooks get executed after method', async () => {
127 | const hook = new Hook.Collection();
128 | const callOrder = [];
129 | const method = function method() {
130 | callOrder.push(1);
131 | throw new Error("oops");
132 | };
133 | let errorHandlerCallCount = 0;
134 | const errorHandler = function errorHandler() {
135 | errorHandlerCallCount++;
136 | throw new Error("error handler oops");
137 | };
138 |
139 | hook.error("test", () => {
140 | callOrder.push(2);
141 | errorHandler();
142 | });
143 | hook.error("test", () => {
144 | callOrder.push(3);
145 | errorHandler();
146 | });
147 |
148 | try {
149 | await hook("test", method);
150 | assert(false, "should not resolve");
151 | } catch (error) {
152 | assert(errorHandlerCallCount === 2);
153 | assert(callOrder.length === 3);
154 | assert(callOrder[0] === 1);
155 | assert(callOrder[1] === 2);
156 | assert(callOrder[2] === 3);
157 | assert(error.message === "error handler oops");
158 | }
159 | });
160 |
--------------------------------------------------------------------------------
/test/hook.test.js:
--------------------------------------------------------------------------------
1 | import { test, assert } from "./testrunner.js";
2 |
3 | import Hook from "../index.js";
4 |
5 | test("hook(name, options, method) multiple names", async () => {
6 | const hook = new Hook.Collection();
7 | const calls = [];
8 |
9 | hook.before("outer", () => {
10 | calls.push("beforeOuter");
11 | });
12 | hook.before("inner", () => {
13 | calls.push("beforeInner");
14 | });
15 | hook.after("inner", () => {
16 | calls.push("afterInner");
17 | });
18 | hook.after("outer", () => {
19 | calls.push("afterOuter");
20 | });
21 |
22 | await hook(["outer", "dafuq", "inner"], () => {
23 | calls.push("method");
24 | });
25 |
26 | assert(calls.length === 5);
27 | assert(calls[0] === "beforeOuter");
28 | assert(calls[1] === "beforeInner");
29 | assert(calls[2] === "method");
30 | assert(calls[3] === "afterInner");
31 | assert(calls[4] === "afterOuter");
32 | });
33 |
34 | test("hook(name, options, method) order", async () => {
35 | const hook = new Hook.Collection();
36 | const calls = [];
37 |
38 | hook.before("test", () => {
39 | calls.push("before test 1");
40 | });
41 | hook.after("test", () => {
42 | calls.push("after test 1");
43 | });
44 | hook.before("test", () => {
45 | calls.push("before test 2");
46 | });
47 | hook.after("test", () => {
48 | calls.push("after test 2");
49 | });
50 |
51 | await hook("test", () => {
52 | calls.push("method");
53 | });
54 |
55 | assert(calls.length === 5);
56 | assert(calls[0] === "before test 2");
57 | assert(calls[1] === "before test 1");
58 | assert(calls[2] === "method");
59 | assert(calls[3] === "after test 1");
60 | assert(calls[4] === "after test 2");
61 | });
62 |
63 | test("hook(name, options, method) no handlers defined (#51)", async () => {
64 | const hook = new Hook.Collection();
65 | const options = { foo: "bar" };
66 |
67 | await hook(
68 | "test",
69 | (_options) => {
70 | assert(typeof _options === "object");
71 | assert(Object.keys(_options).length === 1);
72 | assert(_options.foo === "bar");
73 | },
74 | options
75 | );
76 | });
77 |
--------------------------------------------------------------------------------
/test/remove.test.js:
--------------------------------------------------------------------------------
1 | import { test, assert } from "./testrunner.js";
2 |
3 | import Hook from "../index.js";
4 |
5 | test('hook.remove("test", check)', async () => {
6 | const hook = new Hook.Collection();
7 | const calls = [];
8 |
9 | const afterCheck = () => {
10 | calls.push("after");
11 | };
12 | hook.after("test", afterCheck);
13 | hook.remove("test", afterCheck);
14 |
15 | await hook("test", () => {
16 | calls.push("check");
17 | });
18 |
19 | assert(calls.length === 1);
20 | assert(calls[0] === "check");
21 | });
22 |
23 | test('hook.remove("test", check) without "check" matching existing function', async () => {
24 | const hook = new Hook.Collection();
25 | const calls = [];
26 |
27 | hook.before("test", () => calls.push("before"));
28 | hook.remove("test", () => {
29 | throw new Error("should not be called");
30 | });
31 |
32 | await hook("test", () => {
33 | calls.push("check");
34 | });
35 |
36 | assert(calls.length === 2);
37 | assert(calls[0] === "before");
38 | assert(calls[1] === "check");
39 | });
40 |
--------------------------------------------------------------------------------
/test/singular-hook.test.js:
--------------------------------------------------------------------------------
1 | import { test, assert } from "./testrunner.js";
2 |
3 | import Hook from "../index.js";
4 |
5 | test("Hook.Singular() multiple names", async () => {
6 | const hook = new Hook.Singular();
7 | const calls = [];
8 |
9 | hook.before(() => {
10 | calls.push("beforeSecond");
11 | });
12 | hook.before(() => {
13 | calls.push("beforeFirst");
14 | });
15 | hook.after(() => {
16 | calls.push("afterFirst");
17 | });
18 | hook.after(() => {
19 | calls.push("afterSecond");
20 | });
21 |
22 | await hook(() => {
23 | calls.push("method");
24 | });
25 |
26 | assert(calls.length === 5);
27 | assert(calls[0] === "beforeFirst");
28 | assert(calls[1] === "beforeSecond");
29 | assert(calls[2] === "method");
30 | assert(calls[3] === "afterFirst");
31 | assert(calls[4] === "afterSecond");
32 | });
33 |
34 | test("Hook.Singular() order", async () => {
35 | const hook = new Hook.Singular();
36 | const calls = [];
37 |
38 | hook.before(() => {
39 | calls.push("before 1");
40 | });
41 | hook.after(() => {
42 | calls.push("after 1");
43 | });
44 | hook.before(() => {
45 | calls.push("before 2");
46 | });
47 | hook.after(() => {
48 | calls.push("after 2");
49 | });
50 |
51 | await hook(() => {
52 | calls.push("method");
53 | });
54 |
55 | assert(calls.length === 5);
56 | assert(calls[0] === "before 2");
57 | assert(calls[1] === "before 1");
58 | assert(calls[2] === "method");
59 | assert(calls[3] === "after 1");
60 | assert(calls[4] === "after 2");
61 | });
62 |
63 | test("Hook.Singular() multiple unnamed hooks", async () => {
64 | const calls = [];
65 |
66 | const hook1 = new Hook.Singular();
67 | hook1.before(() => {
68 | calls.push("before 1");
69 | });
70 | hook1.after(() => {
71 | calls.push("after 1");
72 | });
73 | const hook2 = new Hook.Singular();
74 | hook2.before(() => {
75 | calls.push("before 2");
76 | });
77 | hook2.after(() => {
78 | calls.push("after 2");
79 | });
80 |
81 | await hook1(() => {
82 | calls.push("method 1");
83 | });
84 |
85 | await hook2(() => {
86 | calls.push("method 2");
87 | });
88 |
89 | assert(calls.length === 6);
90 | assert(calls[0] === "before 1");
91 | assert(calls[1] === "method 1");
92 | assert(calls[2] === "after 1");
93 | assert(calls[3] === "before 2");
94 | assert(calls[4] === "method 2");
95 | assert(calls[5] === "after 2");
96 | });
97 |
98 | test("Hook.Singular() no handlers defined (#51)", async () => {
99 | const hook = new Hook.Singular();
100 | const options = { foo: "bar" };
101 |
102 | await hook((_options) => {
103 | assert(typeof _options === "object");
104 | assert(Object.keys(_options).length === 1);
105 | assert(_options.foo === "bar", "passes options to method");
106 | }, options);
107 | });
108 |
--------------------------------------------------------------------------------
/test/testrunner.js:
--------------------------------------------------------------------------------
1 | let test, assert;
2 | if ("Bun" in globalThis) {
3 | test = function test(name, fn) {
4 | return globalThis.Bun.jest(caller()).it(name, fn);
5 | };
6 | assert = function assert(value, message) {
7 | return globalThis.Bun.jest(caller()).expect(value, message);
8 | };
9 | /** Retrieve caller test file. */
10 | function caller() {
11 | const Trace = Error;
12 | const _ = Trace.prepareStackTrace;
13 | Trace.prepareStackTrace = (_, stack) => stack;
14 | const { stack } = new Error();
15 | Trace.prepareStackTrace = _;
16 | const caller = stack[2];
17 | return caller.getFileName().replaceAll("\\", "/");
18 | }
19 | } else if ("Deno" in globalThis) {
20 | const nodeTest = await import("node:test");
21 | const nodeAssert = await import("node:assert");
22 |
23 | test = nodeTest.test;
24 | assert = nodeAssert.strict;
25 | } else if (process.env.VITEST_WORKER_ID) {
26 | test = await import("vitest").then((module) => module.test);
27 | assert = await import("vitest").then((module) => module.assert);
28 | } else {
29 | const nodeTest = await import("node:test");
30 | const nodeAssert = await import("node:assert");
31 |
32 | test = nodeTest.test;
33 | assert = nodeAssert.strict;
34 | }
35 |
36 | export { test, assert };
37 |
--------------------------------------------------------------------------------
/test/unit-register.test.js:
--------------------------------------------------------------------------------
1 | import { test, assert } from "./testrunner.js";
2 |
3 | import { register } from "../lib/register.js";
4 |
5 | test('register("name", method) with empty registry and thrown error by method', async () => {
6 | try {
7 | await register(
8 | {
9 | registry: {},
10 | },
11 | "test",
12 | () => {
13 | throw new Error("foo");
14 | }
15 | );
16 | assert(false, "should not resolve");
17 | } catch (error) {
18 | assert(error.message === "foo", "rejects with error message from method");
19 | }
20 | });
21 |
22 | test('register("name", undefined)', () => {
23 | try {
24 | register.bind(null, {}, "test", undefined)();
25 | assert(false, "should throw");
26 | } catch (error) {
27 | assert(
28 | error.message === "method for before hook must be a function",
29 | "rejects with error message from method"
30 | );
31 | }
32 | });
33 |
34 | test('register("name", undefined, method)', () => {
35 | try {
36 | register.bind(null, {}, "test", undefined, () => {})();
37 | assert(false, "should throw");
38 | } catch (error) {
39 | assert(
40 | error.message === "method for before hook must be a function",
41 | "rejects with error message from method"
42 | );
43 | }
44 | });
45 |
--------------------------------------------------------------------------------
/test/unit-remove.test.js:
--------------------------------------------------------------------------------
1 | import { test, assert } from "./testrunner.js";
2 |
3 | import { removeHook } from "../lib/remove.js";
4 |
5 | test('removeHook("before", "name", method) with empty registry', () => {
6 | const state = {
7 | registry: {},
8 | };
9 | removeHook(state, "before", "test", () => {});
10 |
11 | assert(typeof state.registry === "object");
12 | assert(Object.keys(state.registry).length === 0);
13 | });
14 |
15 | test('removeHook("before", "name", method) with method that cannot be found', () => {
16 | const state = {
17 | registry: {
18 | test: [],
19 | },
20 | };
21 | removeHook(state, "before", "test", () => {});
22 | assert(typeof state.registry === "object");
23 | assert(Object.keys(state.registry).length === 1);
24 | assert(Array.isArray(state.registry.test));
25 | assert(state.registry.test.length === 0);
26 | });
27 |
--------------------------------------------------------------------------------
/test/wrap.test.js:
--------------------------------------------------------------------------------
1 | import { test, assert } from "./testrunner.js";
2 |
3 | import Hook from "../index.js";
4 |
5 | test('hook.wrap("test", wrapMethod) before/after/error', async () => {
6 | const hook = new Hook.Collection();
7 | const calls = [];
8 |
9 | hook.wrap("test", (method) => {
10 | calls.push("before");
11 | try {
12 | method();
13 | } catch (error) {
14 | calls.push("error");
15 | }
16 | calls.push("after");
17 | });
18 |
19 | await hook("test", () => {
20 | calls.push("method");
21 | throw new Error("ooops");
22 | });
23 |
24 | assert(calls.length === 4);
25 | assert(calls[0] === "before");
26 | assert(calls[1] === "method");
27 | assert(calls[2] === "error");
28 | assert(calls[3] === "after");
29 | });
30 |
31 | test('hook.wrap("test", wrapMethod) async check', async () => {
32 | const hook = new Hook.Collection();
33 | const calls = [];
34 |
35 | hook.wrap("test", (method) => {
36 | calls.push("before");
37 | return method()
38 | .catch(() => calls.push("error"))
39 | .then(() => calls.push("after"));
40 | });
41 |
42 | await hook("test", async () => {
43 | calls.push("method");
44 | throw new Error("ooops");
45 | });
46 |
47 | assert(calls.length === 4);
48 | assert(calls[0] === "before");
49 | assert(calls[1] === "method");
50 | assert(calls[2] === "error");
51 | assert(calls[3] === "after");
52 | });
53 |
54 | test('hook.wrap("test", wrapMethod) throws error', async () => {
55 | const hook = new Hook.Collection();
56 |
57 | let methodCallCount = 0;
58 | const method = function method() {
59 | methodCallCount++;
60 | };
61 |
62 | hook.wrap("test", () => {
63 | throw new Error("oops");
64 | });
65 |
66 | try {
67 | await hook("test", method);
68 | assert(false, "must not resolve");
69 | } catch (error) {
70 | assert(error.message === "oops", "rejects with error message from check");
71 | assert(methodCallCount === 0);
72 | }
73 | });
74 |
75 | test('hook.wrap("test", wrapMethod) rejected promise', async () => {
76 | const hook = new Hook.Collection();
77 |
78 | let methodCallCount = 0;
79 | const method = function method() {
80 | methodCallCount++;
81 | };
82 |
83 | hook.wrap("test", () => {
84 | return Promise.reject(new Error("oops"));
85 | });
86 |
87 | try {
88 | await hook("test", method);
89 | hook("test", method);
90 | assert(false, "must not resolve");
91 | } catch (error) {
92 | assert(error.message, "oops", "rejects with error message from check");
93 | assert(methodCallCount === 0);
94 | }
95 | });
96 |
97 | test('hook.wrap("test", wrapMethod) options', async () => {
98 | const hook = new Hook.Collection();
99 |
100 | hook.wrap("test", (method, options) => {
101 | options.foo = "bar";
102 | return method(options);
103 | });
104 | hook.wrap("test", (method, options) => {
105 | options.baz = "ar";
106 | return method(options);
107 | });
108 |
109 | await hook(
110 | "test",
111 | (options) => {
112 | assert(options.foo === "bar", "passes options to before hook");
113 | assert(options.baz === "ar", "passes options to before hook");
114 | assert(options.otherbar === "baz", "passes options to before hook");
115 | },
116 | { foo: "notbar", otherbar: "baz" }
117 | );
118 | });
119 |
120 | test('hook.wrap("test", wrapMethod) multiple wrap hooks', async () => {
121 | const hook = new Hook.Collection();
122 | const calls = [];
123 |
124 | hook.wrap("test", (method) => {
125 | calls.push("wrap1");
126 | method();
127 | });
128 | hook.wrap("test", (method) => {
129 | calls.push("wrap2");
130 | method();
131 | });
132 |
133 | await await hook("test", () => {
134 | calls.push("method");
135 | });
136 |
137 | assert(calls.length === 3);
138 | assert(calls[0] === "wrap2");
139 | assert(calls[1] === "wrap1");
140 | assert(calls[2] === "method");
141 | });
142 |
--------------------------------------------------------------------------------