├── .github
├── FUNDING.yml
├── contributing.md
├── issue_template.md
├── pull_request_template.md
└── workflows
│ └── deno.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── deno.json
├── deno.lock
├── lerna.json
├── main
├── build.ts
└── hooks
│ ├── LICENSE
│ ├── README.md
│ ├── package.json.ts
│ ├── src
│ ├── base.ts
│ ├── compose.ts
│ ├── hooks.ts
│ ├── index.ts
│ ├── regular.ts
│ └── utils.ts
│ └── test
│ ├── benchmark.test.ts
│ ├── class.test.ts
│ ├── collect.test.ts
│ ├── compose.test.ts
│ ├── decorator.test.ts
│ ├── dependencies.ts
│ ├── function.test.ts
│ └── object.test.ts
├── makefile
├── package-lock.json
├── package.json
└── readme.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [ 'daffl', 'bertho-zero' ]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: feathers
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | custom: # Replace with a single custom sponsorship URL
10 |
--------------------------------------------------------------------------------
/.github/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing to Feathers
2 |
3 | Thank you for contributing to Feathers! :heart: :tada:
4 |
5 | Feathers embraces modularity and is broken up across multiple modules. You can find them all in the `packages/` folder. Most reflect their name on npm. For example, the code for `@feathersjs/feathers` will be in `packages/feathers`, for `@feathersjs/authentication` in `packages/authenticaiton`.
6 |
7 | ## Report a bug
8 |
9 | Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question.
10 |
11 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github.
12 |
13 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**.
14 |
15 | ## Report a Security Concern
16 |
17 | We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default.
18 |
19 | In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP.
20 |
21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html).
22 |
23 | ## Pull Requests
24 |
25 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute.
26 |
27 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A FeathersJS maintainer will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it.
28 |
29 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context, historical knowledge or information regarding the roadmap that the maintainers have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile:
30 |
31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.**
32 |
33 | ### Code style
34 |
35 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`.
36 |
37 | ### Tests
38 |
39 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command.
40 |
41 | ### Documentation
42 |
43 | Feathers documentation is contained in Markdown files in the [docs](https://github.com/feathersjs/docs) repository. To change the documentation submit a pull request to that repo, referencing any other PR if applicable, and the docs will be updated with the next release.
44 |
45 | ## Community Contributions
46 |
47 | If you've written something awesome about Feathers, for the Feathers ecosystem, or created an app using Feathers please add it to the [awesome-feathersjs](https://github.com/feathersjs-ecosystem/awesome-feathersjs).
48 |
49 | If you are looking to create a new plugin you also might want to check out the [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) that can be used to scaffold plugins to be Feathers compliant from the start.
50 |
51 | If you think your module would be a good core `feathersjs` module or `featherjs-ecosystem` module then please contact one of the Feathers maintainers in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs and how to get it there. :beers:
52 |
53 | ## Contributor Code of Conduct
54 |
55 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
56 |
57 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
58 |
59 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
60 |
61 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
62 |
63 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
64 |
65 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
66 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 | ### Steps to reproduce
2 |
3 | (First please check that this issue is not already solved as [described
4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug))
5 |
6 | - [ ] Tell us what broke. The more detailed the better.
7 | - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. This makes it much easier for us to debug and issues that have a reproducable example will get higher priority.
8 |
9 | ### Expected behavior
10 | Tell us what should happen
11 |
12 | ### Actual behavior
13 | Tell us what happens instead
14 |
15 | ### System configuration
16 |
17 | Tell us about the applicable parts of your setup.
18 |
19 | **Module versions** (especially the part that's not working):
20 |
21 | **NodeJS version**:
22 |
23 | **Operating System**:
24 |
25 | **Browser Version**:
26 |
27 | **React Native Version**:
28 |
29 | **Module Loader**:
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ### Summary
2 |
3 | (If you have not already please refer to the contributing guideline as [described
4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests))
5 |
6 | - [ ] Tell us about the problem your pull request is solving.
7 | - [ ] Are there any open issues that are related to this?
8 | - [ ] Is this PR dependent on PRs in other repos?
9 |
10 | If so, please mention them to keep the conversations linked together.
11 |
12 | ### Other Information
13 |
14 | If there's anything else that's important and relevant to your pull
15 | request, mention that information here. This could include
16 | benchmarks, or other information.
17 |
18 | Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release.
19 |
20 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs).
21 |
22 | Thanks for contributing to Feathers! :heart:
--------------------------------------------------------------------------------
/.github/workflows/deno.yml:
--------------------------------------------------------------------------------
1 | name: Deno CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | runs-on: "ubuntu-latest"
8 | steps:
9 | - name: Setup repo
10 | uses: actions/checkout@v2
11 |
12 | - name: Setup Deno
13 | uses: denolib/setup-deno@v2
14 | with:
15 | deno-version: v1.x
16 |
17 | - name: Run Tests
18 | run: make test
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | # Build folders
64 | lib/
65 | dist/
66 | *.sqlite
67 |
68 | /packages/
69 | /coverage/
70 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | # [0.9.0](https://github.com/feathersjs/hooks/compare/v0.8.1...v0.9.0) (2024-02-25)
7 |
8 |
9 | ### Bug Fixes
10 |
11 | * Use Symbol.for to register a single instance of all hooks ([#110](https://github.com/feathersjs/hooks/issues/110)) ([92a342e](https://github.com/feathersjs/hooks/commit/92a342e47253c162536f85342ddb1d90154b74ef))
12 |
13 |
14 | ### Features
15 |
16 | * Move to TypeScript 5 non-experimental decorators ([#109](https://github.com/feathersjs/hooks/issues/109)) ([05bde46](https://github.com/feathersjs/hooks/commit/05bde4606635b8c43f11778867e7a3bc11f3c6a2))
17 |
18 |
19 |
20 |
21 |
22 | ## [0.8.1](https://github.com/feathersjs/hooks/compare/v0.8.0...v0.8.1) (2023-02-10)
23 |
24 |
25 | ### Bug Fixes
26 |
27 | * **hooks:** Add toJSON to hook context ([#108](https://github.com/feathersjs/hooks/issues/108)) ([03e5883](https://github.com/feathersjs/hooks/commit/03e588344fb54066b8a3b178ca114d5925432745))
28 |
29 |
30 |
31 |
32 |
33 | # [0.8.0](https://github.com/feathersjs/hooks/compare/v0.7.6...v0.8.0) (2023-02-07)
34 |
35 |
36 | ### Bug Fixes
37 |
38 | * **hooks:** Update context.error in error hooks ([#107](https://github.com/feathersjs/hooks/issues/107)) ([d148869](https://github.com/feathersjs/hooks/commit/d148869c4f41e700e92e0d7e14a19b63690c4521))
39 |
40 |
41 | ### Features
42 |
43 | * **hooks:** Performance improvement ([#106](https://github.com/feathersjs/hooks/issues/106)) ([c14d07c](https://github.com/feathersjs/hooks/commit/c14d07c0cb4674056c6aadb14dd6f808399a95a3))
44 |
45 |
46 |
47 |
48 |
49 | ## [0.7.6](https://github.com/feathersjs/hooks/compare/v0.7.5...v0.7.6) (2022-12-08)
50 |
51 |
52 | ### Bug Fixes
53 |
54 | * restore `context.type` ([#104](https://github.com/feathersjs/hooks/issues/104)) ([1d934ae](https://github.com/feathersjs/hooks/commit/1d934aee85276b4585268dcaf4e6360d7b1137dc))
55 |
56 |
57 |
58 |
59 |
60 | ## [0.7.5](https://github.com/feathersjs/hooks/compare/v0.7.4...v0.7.5) (2022-06-04)
61 |
62 |
63 | ### Bug Fixes
64 |
65 | * **hooks:** Ensure that all error hooks are run ([#103](https://github.com/feathersjs/hooks/issues/103)) ([bbd1979](https://github.com/feathersjs/hooks/commit/bbd1979a1854441def03581bc248b11bb5fb1b2d))
66 | * **hooks:** Update regular hook order as suggested in [#97](https://github.com/feathersjs/hooks/issues/97) ([9b758fc](https://github.com/feathersjs/hooks/commit/9b758fc6e498c3f4f9a85da47974c6c2244d588e))
67 |
68 |
69 |
70 |
71 |
72 | ## [0.7.4](https://github.com/feathersjs/hooks/compare/v0.7.3...v0.7.4) (2022-04-27)
73 |
74 |
75 | ### Bug Fixes
76 |
77 | * **hooks:** Allow hooks on functions that have properties ([#102](https://github.com/feathersjs/hooks/issues/102)) ([eee7880](https://github.com/feathersjs/hooks/commit/eee7880918769c7dbcd2b48853fea0148dc23951))
78 |
79 |
80 |
81 |
82 |
83 | ## [0.7.3](https://github.com/feathersjs/hooks/compare/v0.7.2...v0.7.3) (2022-02-05)
84 |
85 |
86 | ### Bug Fixes
87 |
88 | * **build:** Update DNT build system to fix Webpack builds ([#99](https://github.com/feathersjs/hooks/issues/99)) ([7b7033b](https://github.com/feathersjs/hooks/commit/7b7033b76c1f678352fff96a4873a88405c6ec76))
89 |
90 |
91 |
92 |
93 |
94 | ## [0.7.2](https://github.com/feathersjs/hooks/compare/v0.7.1...v0.7.2) (2021-11-20)
95 |
96 |
97 | ### Bug Fixes
98 |
99 | * Use proper Deno location ([c70b070](https://github.com/feathersjs/hooks/commit/c70b0706d6e2910cdfc6cf9f9dc71b63ed605fc7))
100 |
101 |
102 |
103 |
104 |
105 | ## [0.7.1](https://github.com/feathersjs/hooks/compare/v0.7.0...v0.7.1) (2021-11-20)
106 |
107 |
108 | ### Bug Fixes
109 |
110 | * Update Deno package location ([b2ae6eb](https://github.com/feathersjs/hooks/commit/b2ae6eb4785052c0004d721163751321295c3207))
111 |
112 |
113 |
114 |
115 |
116 | # [0.7.0](https://github.com/feathersjs/hooks/compare/v0.6.5...v0.7.0) (2021-11-19)
117 |
118 |
119 | ### Features
120 |
121 | * Add support for regular before, after and error hooks ([#92](https://github.com/feathersjs/hooks/issues/92)) ([8e4328f](https://github.com/feathersjs/hooks/commit/8e4328f0b6963305e81d64ce0a06dbfab56e594a))
122 | * Move development and publishing to Deno ([#93](https://github.com/feathersjs/hooks/issues/93)) ([c2a1b0b](https://github.com/feathersjs/hooks/commit/c2a1b0b03a4ba320df90d054c0b2fedbc382d6fe))
123 |
124 |
125 |
126 |
127 |
128 |
129 | ## [0.6.5](https://github.com/feathersjs/hooks/compare/v0.6.4...v0.6.5) (2021-04-20)
130 |
131 |
132 | ### Bug Fixes
133 |
134 | * **typescript:** Tighten up TypeScript settings to be ECMAScript and Deno compatible ([#81](https://github.com/feathersjs/hooks/issues/81)) ([28fe875](https://github.com/feathersjs/hooks/commit/28fe8758b4764981473830db4a0013dd1beca489))
135 |
136 |
137 |
138 |
139 |
140 | ## [0.6.4](https://github.com/feathersjs/hooks/compare/v0.6.3...v0.6.4) (2021-04-11)
141 |
142 |
143 | ### Bug Fixes
144 |
145 | * allow to hooks a function without middleware ([#77](https://github.com/feathersjs/hooks/issues/77)) ([38b44c3](https://github.com/feathersjs/hooks/commit/38b44c3ba1bd7753cdb81492b517e4fd3a6af50e))
146 | * conserve fn.length and fn.name ([#79](https://github.com/feathersjs/hooks/issues/79)) ([d9bc9af](https://github.com/feathersjs/hooks/commit/d9bc9af689f15398168ce4493fcfb23af0f3ef05))
147 |
148 |
149 |
150 |
151 |
152 | ## [0.6.3](https://github.com/feathersjs/hooks/compare/v0.6.2...v0.6.3) (2021-03-31)
153 |
154 |
155 | ### Bug Fixes
156 |
157 | * **hooks:** Add Deno tests and build, CI and fix build ([#73](https://github.com/feathersjs/hooks/issues/73)) ([44787cd](https://github.com/feathersjs/hooks/commit/44787cd2106c6d1ff4fe8bc5d59362e14427c468))
158 |
159 |
160 |
161 |
162 |
163 | ## [0.6.2](https://github.com/feathersjs/hooks/compare/v0.6.1...v0.6.2) (2021-02-08)
164 |
165 |
166 | ### Bug Fixes
167 |
168 | * **hooks:** Allow to set context.result to undefined ([#70](https://github.com/feathersjs/hooks/issues/70)) ([7b5807f](https://github.com/feathersjs/hooks/commit/7b5807f8a31b0e4859eaabdbcc8b49fc3b544548))
169 |
170 |
171 |
172 |
173 |
174 | ## [0.6.1](https://github.com/feathersjs/hooks/compare/v0.6.0...v0.6.1) (2020-12-11)
175 |
176 |
177 | ### Bug Fixes
178 |
179 | * **hooks:** fix some errors for feathers integration ([#67](https://github.com/feathersjs/hooks/issues/67)) ([fcfc0ca](https://github.com/feathersjs/hooks/commit/fcfc0ca6423a8062959d41f34e673f81d3c006dd))
180 | * **hooks:** Remove redundant method call ([#65](https://github.com/feathersjs/hooks/issues/65)) ([4ff10a9](https://github.com/feathersjs/hooks/commit/4ff10a9935682276b8ca3ffb699275b627230dfa))
181 | * **hooks:** Stricter condition ([#64](https://github.com/feathersjs/hooks/issues/64)) ([6de77a1](https://github.com/feathersjs/hooks/commit/6de77a1afcbee4867b7e464be0b556a8dc9656e3))
182 |
183 |
184 |
185 |
186 |
187 | # [0.6.0](https://github.com/feathersjs/hooks/compare/v0.5.0...v0.6.0) (2020-11-12)
188 |
189 |
190 | ### Features
191 |
192 | * **hooks:** Revert refactoring into separate hooks (PR [#37](https://github.com/feathersjs/hooks/issues/37)) ([#57](https://github.com/feathersjs/hooks/issues/57)) ([56a44be](https://github.com/feathersjs/hooks/commit/56a44beb3388873f7bef12ac640f115beffceb95))
193 |
194 |
195 |
196 |
197 |
198 | # [0.5.0](https://github.com/feathersjs/hooks/compare/v0.5.0-alpha.0...v0.5.0) (2020-06-01)
199 |
200 |
201 | ### Features
202 |
203 | * **hooks:** Finalize default initializer functionality ([#35](https://github.com/feathersjs/hooks/issues/35)) ([d380d76](https://github.com/feathersjs/hooks/commit/d380d76891b28160c992438bfb3f28493eacddc4))
204 | * **hooks:** Refactor .params, .props and .defaults into hooks ([#37](https://github.com/feathersjs/hooks/issues/37)) ([9b13b7d](https://github.com/feathersjs/hooks/commit/9b13b7de6b708a2152927335aae25dd320b4cfeb))
205 | * **typescript:** Improve type indexes for stricter object and class hooks ([699f2fd](https://github.com/feathersjs/hooks/commit/699f2fd973eb72c0d7c3aefff7b230a7a8a2918a))
206 |
207 |
208 |
209 |
210 |
211 | # [0.5.0-alpha.0](https://github.com/feathersjs/hooks/compare/v0.4.0-alpha.0...v0.5.0-alpha.0) (2020-04-05)
212 |
213 | **Note:** Version bump only for package @feathersjs/hooks
214 |
215 |
216 |
217 |
218 |
219 | # [0.4.0-alpha.0](https://github.com/feathersjs/hooks/compare/v0.3.1...v0.4.0-alpha.0) (2020-02-19)
220 |
221 |
222 | ### Bug Fixes
223 |
224 | * conserve props from original method ([#19](https://github.com/feathersjs/hooks/issues/19)) ([9a77e81](https://github.com/feathersjs/hooks/commit/9a77e81a8b0912a8d3275a2d18e19616d4e4d37e))
225 | * remove walkOriginal ([df1f7ff](https://github.com/feathersjs/hooks/commit/df1f7ffa73ea087d487582efa3c8c7f5be992ab9))
226 | * Update withParams ([#16](https://github.com/feathersjs/hooks/issues/16)) ([b89d044](https://github.com/feathersjs/hooks/commit/b89d0443680d1a30f0875d1b817ddf9ad6220ffe))
227 | * use collector of each .original ([#17](https://github.com/feathersjs/hooks/issues/17)) ([33fd2cb](https://github.com/feathersjs/hooks/commit/33fd2cb3a66301e76be6e83c5a7d6248434c7fd0))
228 |
229 |
230 | ### Features
231 |
232 | * add setMiddleware ([#18](https://github.com/feathersjs/hooks/issues/18)) ([7d0113d](https://github.com/feathersjs/hooks/commit/7d0113d4e6c972983e6384ff892cb5ca8c70365c))
233 | * Chainable configuration ([#23](https://github.com/feathersjs/hooks/issues/23)) ([c534827](https://github.com/feathersjs/hooks/commit/c534827d539faab885f84d035e2edb912770759f))
234 |
235 |
236 |
237 |
238 |
239 | ## [0.3.1](https://github.com/feathersjs/hooks/compare/v0.3.0...v0.3.1) (2020-01-29)
240 |
241 |
242 | ### Bug Fixes
243 |
244 | * Fix dependency entries ([4a15e74](https://github.com/feathersjs/hooks/commit/4a15e74f83247833edf7de8ea26b908115a5ab7a))
245 |
246 |
247 |
248 |
249 |
250 | # [0.3.0](https://github.com/feathersjs/hooks/compare/v0.2.0...v0.3.0) (2020-01-29)
251 |
252 |
253 | ### Features
254 |
255 | * Allow multiple context initializers ([#12](https://github.com/feathersjs/hooks/issues/12)) ([a556272](https://github.com/feathersjs/hooks/commit/a556272f535c7d2a25bcbc12d8473cdaefaf8c56))
256 |
257 |
258 |
259 |
260 |
261 | # [0.2.0](https://github.com/feathersjs/hooks/compare/v0.1.0...v0.2.0) (2020-01-14)
262 |
263 |
264 | ### Bug Fixes
265 |
266 | * Add tests for fn.original and update documentation ([#5](https://github.com/feathersjs/hooks/issues/5)) ([f4c1955](https://github.com/feathersjs/hooks/commit/f4c195512c2f24d4d9abb68d39275f2287574162))
267 |
268 |
269 | ### Features
270 |
271 | * Add browser build ([#8](https://github.com/feathersjs/hooks/issues/8)) ([d6162ca](https://github.com/feathersjs/hooks/commit/d6162caccabe43c468df9360f7f03362ad36c41d))
272 | * Add build script and publish a version for Deno ([#6](https://github.com/feathersjs/hooks/issues/6)) ([f2b5697](https://github.com/feathersjs/hooks/commit/f2b56972fa2ef21799bc6e531644ef9e751bd25b))
273 | * Refactoring to pass an option object to initialize hooks more explicitly ([#7](https://github.com/feathersjs/hooks/issues/7)) ([8f2453f](https://github.com/feathersjs/hooks/commit/8f2453f3e230f6c17989f244cc3dc8126a895eeb))
274 |
275 |
276 |
277 |
278 |
279 | # 0.1.0 (2020-01-05)
280 |
281 |
282 | ### Features
283 |
284 | * Finalize functionality for initial release of @feathersjs/hooks package ([#1](https://github.com/feathersjs/hooks/issues/1)) ([edab7a1](https://github.com/feathersjs/hooks/commit/edab7a1d24b2f25f59af01aad1275ea74dee3879))
285 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Feathers
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 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "lint": {
3 | "files": {
4 | "include": ["main"],
5 | "exclude": []
6 | },
7 | "rules": {
8 | "tags": ["recommended"],
9 | "include": [],
10 | "exclude": ["no-explicit-any", "require-await", "no-self-assign"]
11 | }
12 | },
13 | "fmt": {
14 | "files": {
15 | "include": ["main/"],
16 | "exclude": []
17 | },
18 | "options": {
19 | "useTabs": false,
20 | "lineWidth": 120,
21 | "indentWidth": 2,
22 | "singleQuote": true
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/deno.lock:
--------------------------------------------------------------------------------
1 | {
2 | "version": "3",
3 | "remote": {
4 | "https://deno.land/std@0.106.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58",
5 | "https://deno.land/std@0.106.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac",
6 | "https://deno.land/std@0.106.0/fs/_util.ts": "f2ce811350236ea8c28450ed822a5f42a0892316515b1cd61321dec13569c56b",
7 | "https://deno.land/std@0.106.0/fs/ensure_dir.ts": "b7c103dc41a3d1dbbb522bf183c519c37065fdc234831a4a0f7d671b1ed5fea7",
8 | "https://deno.land/std@0.106.0/fs/expand_glob.ts": "73e7b13f01097b04ed782b3d63863379b718417417758ba622e282b1e5300b91",
9 | "https://deno.land/std@0.106.0/fs/walk.ts": "b91c655c60d048035f9cae0e6177991ab3245e786e3ab7d20a5b60012edf2126",
10 | "https://deno.land/std@0.106.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853",
11 | "https://deno.land/std@0.106.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4",
12 | "https://deno.land/std@0.106.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b",
13 | "https://deno.land/std@0.106.0/path/common.ts": "eaf03d08b569e8a87e674e4e265e099f237472b6fd135b3cbeae5827035ea14a",
14 | "https://deno.land/std@0.106.0/path/glob.ts": "3b84af55c53febacf6afe214c095624b22a56b6f57d7312157479cc783a0de65",
15 | "https://deno.land/std@0.106.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12",
16 | "https://deno.land/std@0.106.0/path/posix.ts": "b81974c768d298f8dcd2c720229639b3803ca4a241fa9a355c762fa2bc5ef0c1",
17 | "https://deno.land/std@0.106.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c",
18 | "https://deno.land/std@0.106.0/path/win32.ts": "f4a3d4a3f2c9fe894da046d5eac48b5e789a0ebec5152b2c0985efe96a9f7ae1",
19 | "https://deno.land/std@0.111.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58",
20 | "https://deno.land/std@0.111.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac",
21 | "https://deno.land/std@0.111.0/bytes/bytes_list.ts": "3bff6a09c72b2e0b1e92e29bd3b135053894196cca07a2bba842901073efe5cb",
22 | "https://deno.land/std@0.111.0/bytes/equals.ts": "69f55fdbd45c71f920c1a621e6c0865dc780cd8ae34e0f5e55a9497b70c31c1b",
23 | "https://deno.land/std@0.111.0/bytes/mod.ts": "fedb80b8da2e7ad8dd251148e65f92a04c73d6c5a430b7d197dc39588c8dda6f",
24 | "https://deno.land/std@0.111.0/fmt/colors.ts": "8368ddf2d48dfe413ffd04cdbb7ae6a1009cf0dccc9c7ff1d76259d9c61a0621",
25 | "https://deno.land/std@0.111.0/fs/_util.ts": "f2ce811350236ea8c28450ed822a5f42a0892316515b1cd61321dec13569c56b",
26 | "https://deno.land/std@0.111.0/fs/ensure_dir.ts": "b7c103dc41a3d1dbbb522bf183c519c37065fdc234831a4a0f7d671b1ed5fea7",
27 | "https://deno.land/std@0.111.0/hash/sha256.ts": "bd85257c68d1fdd9da8457284c4fbb04efa9f4f2229b5f41a638d5b71a3a8d5c",
28 | "https://deno.land/std@0.111.0/io/buffer.ts": "fdf93ba9e5d20ff3369e2c42443efd89131f8a73066f7f59c033cc588a0e2cfe",
29 | "https://deno.land/std@0.111.0/io/types.d.ts": "89a27569399d380246ca7cdd9e14d5e68459f11fb6110790cc5ecbd4ee7f3215",
30 | "https://deno.land/std@0.111.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853",
31 | "https://deno.land/std@0.111.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4",
32 | "https://deno.land/std@0.111.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b",
33 | "https://deno.land/std@0.111.0/path/common.ts": "f41a38a0719a1e85aa11c6ba3bea5e37c15dd009d705bd8873f94c833568cbc4",
34 | "https://deno.land/std@0.111.0/path/glob.ts": "ea87985765b977cc284b92771003b2070c440e0807c90e1eb0ff3e095911a820",
35 | "https://deno.land/std@0.111.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12",
36 | "https://deno.land/std@0.111.0/path/posix.ts": "34349174b9cd121625a2810837a82dd8b986bbaaad5ade690d1de75bbb4555b2",
37 | "https://deno.land/std@0.111.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c",
38 | "https://deno.land/std@0.111.0/path/win32.ts": "11549e8c6df8307a8efcfa47ad7b2a75da743eac7d4c89c9723a944661c8bd2e",
39 | "https://deno.land/std@0.111.0/streams/conversion.ts": "fe0059ed9d3c53eda4ba44eb71a6a9acb98c5fdb5ba1b6c6ab28004724c7641b",
40 | "https://deno.land/std@0.114.0/fmt/colors.ts": "8368ddf2d48dfe413ffd04cdbb7ae6a1009cf0dccc9c7ff1d76259d9c61a0621",
41 | "https://deno.land/std@0.114.0/testing/_diff.ts": "95578951cd3c9977fd3822c64e9831c739ae8d2a5faece71d3f21bfb553f6275",
42 | "https://deno.land/std@0.114.0/testing/asserts.ts": "550c71c634c608cffabf9e50520435cf03ff4bbd1db1253b0ade4debb444b131",
43 | "https://deno.land/std@0.115.1/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58",
44 | "https://deno.land/std@0.115.1/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac",
45 | "https://deno.land/std@0.115.1/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853",
46 | "https://deno.land/std@0.115.1/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4",
47 | "https://deno.land/std@0.115.1/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b",
48 | "https://deno.land/std@0.115.1/path/common.ts": "f41a38a0719a1e85aa11c6ba3bea5e37c15dd009d705bd8873f94c833568cbc4",
49 | "https://deno.land/std@0.115.1/path/glob.ts": "ea87985765b977cc284b92771003b2070c440e0807c90e1eb0ff3e095911a820",
50 | "https://deno.land/std@0.115.1/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12",
51 | "https://deno.land/std@0.115.1/path/posix.ts": "34349174b9cd121625a2810837a82dd8b986bbaaad5ade690d1de75bbb4555b2",
52 | "https://deno.land/std@0.115.1/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c",
53 | "https://deno.land/std@0.115.1/path/win32.ts": "11549e8c6df8307a8efcfa47ad7b2a75da743eac7d4c89c9723a944661c8bd2e",
54 | "https://deno.land/std@0.119.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58",
55 | "https://deno.land/std@0.119.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac",
56 | "https://deno.land/std@0.119.0/fmt/colors.ts": "8368ddf2d48dfe413ffd04cdbb7ae6a1009cf0dccc9c7ff1d76259d9c61a0621",
57 | "https://deno.land/std@0.119.0/fs/empty_dir.ts": "5f08b263dd064dc7917c4bbeb13de0f5505a664b9cdfe312fa86e7518cfaeb84",
58 | "https://deno.land/std@0.119.0/fs/expand_glob.ts": "f6f64ef54addcc84c9012d6dfcf66d39ecc2565e40c4d2b7644d585fe4d12a04",
59 | "https://deno.land/std@0.119.0/fs/walk.ts": "31464d75099aa3fc7764212576a8772dfabb2692783e6eabb910f874a26eac54",
60 | "https://deno.land/std@0.119.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853",
61 | "https://deno.land/std@0.119.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4",
62 | "https://deno.land/std@0.119.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b",
63 | "https://deno.land/std@0.119.0/path/common.ts": "f41a38a0719a1e85aa11c6ba3bea5e37c15dd009d705bd8873f94c833568cbc4",
64 | "https://deno.land/std@0.119.0/path/glob.ts": "ea87985765b977cc284b92771003b2070c440e0807c90e1eb0ff3e095911a820",
65 | "https://deno.land/std@0.119.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12",
66 | "https://deno.land/std@0.119.0/path/posix.ts": "34349174b9cd121625a2810837a82dd8b986bbaaad5ade690d1de75bbb4555b2",
67 | "https://deno.land/std@0.119.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c",
68 | "https://deno.land/std@0.119.0/path/win32.ts": "11549e8c6df8307a8efcfa47ad7b2a75da743eac7d4c89c9723a944661c8bd2e",
69 | "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
70 | "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49",
71 | "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d",
72 | "https://deno.land/std@0.140.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9",
73 | "https://deno.land/std@0.140.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf",
74 | "https://deno.land/std@0.140.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37",
75 | "https://deno.land/std@0.140.0/fs/_util.ts": "0fb24eb4bfebc2c194fb1afdb42b9c3dda12e368f43e8f2321f84fc77d42cb0f",
76 | "https://deno.land/std@0.140.0/fs/ensure_dir.ts": "9dc109c27df4098b9fc12d949612ae5c9c7169507660dcf9ad90631833209d9d",
77 | "https://deno.land/std@0.140.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b",
78 | "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3",
79 | "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09",
80 | "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b",
81 | "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633",
82 | "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee",
83 | "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d",
84 | "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44",
85 | "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
86 | "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757",
87 | "https://deno.land/std@0.140.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21",
88 | "https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
89 | "https://deno.land/std@0.181.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
90 | "https://deno.land/std@0.181.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
91 | "https://deno.land/std@0.181.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32",
92 | "https://deno.land/std@0.181.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688",
93 | "https://deno.land/std@0.181.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40",
94 | "https://deno.land/std@0.181.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a",
95 | "https://deno.land/std@0.181.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32",
96 | "https://deno.land/std@0.181.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
97 | "https://deno.land/std@0.181.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
98 | "https://deno.land/std@0.181.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
99 | "https://deno.land/std@0.181.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
100 | "https://deno.land/std@0.181.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
101 | "https://deno.land/std@0.181.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c",
102 | "https://deno.land/std@0.181.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
103 | "https://deno.land/std@0.181.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
104 | "https://deno.land/std@0.181.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
105 | "https://deno.land/x/code_block_writer@11.0.0/comment_char.ts": "22b66890bbdf7a2d59777ffd8231710c1fda1c11fadada67632a596937a1a314",
106 | "https://deno.land/x/code_block_writer@11.0.0/mod.ts": "dc43d56c3487bae02886a09754fb09c607da4ea866817e80f3e60632f3391d70",
107 | "https://deno.land/x/code_block_writer@11.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff",
108 | "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5",
109 | "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff",
110 | "https://deno.land/x/deno_cache@0.2.1/auth_tokens.ts": "01b94d25abd974153a3111653998b9a43c66d84a0e4b362fc5f4bbbf40a6e0f7",
111 | "https://deno.land/x/deno_cache@0.2.1/cache.ts": "67e301c20161546fea45405316314f4c3d85cc7a367b2fb72042903f308f55b7",
112 | "https://deno.land/x/deno_cache@0.2.1/deno_dir.ts": "e4dc68da5641aa337bcc06fb1df28fcb086b366dcbea7d8aaed7ac7c853fedb1",
113 | "https://deno.land/x/deno_cache@0.2.1/deps.ts": "2ebaba0ad86fff8b9027c6afd4c3909a17cd8bf8c9e263151c980c15c56a18ee",
114 | "https://deno.land/x/deno_cache@0.2.1/dirs.ts": "e07003fabed7112375d4a50040297aae768f9d06bb6c2655ca46880653b576b4",
115 | "https://deno.land/x/deno_cache@0.2.1/disk_cache.ts": "d7a361f0683a032bcca28513a7bbedc28c77cfcc6719e6f6cea156c0ff1108df",
116 | "https://deno.land/x/deno_cache@0.2.1/file_fetcher.ts": "352702994c190c45215f3b8086621e117e88bc2174b020faefb5eca653d71d6a",
117 | "https://deno.land/x/deno_cache@0.2.1/http_cache.ts": "af1500149496e2d0acadec24569e2a9c86a3f600cceef045dcf6f5ce8de72b3a",
118 | "https://deno.land/x/deno_cache@0.2.1/mod.ts": "709ab9d1068be5fd77b020b33e7a9394f1e9b453553b1e2336b72c90283cf3c0",
119 | "https://deno.land/x/deno_cache@0.2.1/util.ts": "652479928551259731686686ff2df6f26bc04e8e4d311137b2bf3bc10f779f48",
120 | "https://deno.land/x/deno_cache@0.6.2/auth_tokens.ts": "5d1d56474c54a9d152e44d43ea17c2e6a398dd1e9682c69811a313567c01ee1e",
121 | "https://deno.land/x/deno_cache@0.6.2/cache.ts": "58b53c128b742757efcad10af9a3871f23b4e200674cb5b0ddf61164fb9b2fe7",
122 | "https://deno.land/x/deno_cache@0.6.2/deno_dir.ts": "1ea355b8ba11c630d076b222b197cfc937dd81e5a4a260938997da99e8ff93a0",
123 | "https://deno.land/x/deno_cache@0.6.2/deps.ts": "12cca94516cf2d3ed42fccd4b721ecd8060679253f077d83057511045b0081aa",
124 | "https://deno.land/x/deno_cache@0.6.2/dirs.ts": "009c6f54e0b610914d6ce9f72f6f6ccfffd2d47a79a19061e0a9eb4253836069",
125 | "https://deno.land/x/deno_cache@0.6.2/disk_cache.ts": "66a1e604a8d564b6dd0500326cac33d08b561d331036bf7272def80f2f7952aa",
126 | "https://deno.land/x/deno_cache@0.6.2/file_fetcher.ts": "4f3e4a2c78a5ca1e4812099e5083f815a8525ab20d389b560b3517f6b1161dd6",
127 | "https://deno.land/x/deno_cache@0.6.2/http_cache.ts": "407135eaf2802809ed373c230d57da7ef8dff923c4abf205410b9b99886491fd",
128 | "https://deno.land/x/deno_cache@0.6.2/lib/deno_cache_dir.generated.js": "59f8defac32e8ebf2a30f7bc77e9d88f0e60098463fb1b75e00b9791a4bbd733",
129 | "https://deno.land/x/deno_cache@0.6.2/lib/snippets/deno_cache_dir-a2aecaa9536c9402/fs.js": "cbe3a976ed63c72c7cb34ef845c27013033a3b11f9d8d3e2c4aa5dda2c0c7af6",
130 | "https://deno.land/x/deno_cache@0.6.2/mod.ts": "b4004287e1c6123d7f07fe9b5b3e94ce6d990c4102949a89c527c68b19627867",
131 | "https://deno.land/x/deno_cache@0.6.2/util.ts": "f3f5a0cfc60051f09162942fb0ee87a0e27b11a12aec4c22076e3006be4cc1e2",
132 | "https://deno.land/x/deno_graph@0.6.0/lib/deno_graph.generated.js": "3e1cccd6376d4ad0ea789d66aa0f6b19f737fa8da37b5e6185ef5c269c974f54",
133 | "https://deno.land/x/deno_graph@0.6.0/lib/loader.ts": "13a11c1dea0d85e0ad211be77217b8c06138bbb916afef6f50a04cca415084a9",
134 | "https://deno.land/x/deno_graph@0.6.0/lib/media_type.ts": "36be751aa63d6ae36475b90dca5fae8fd7c3a77cf13684c48cf23a85ee607b31",
135 | "https://deno.land/x/deno_graph@0.6.0/lib/snippets/deno_graph-1c138d6136337537/src/deno_apis.js": "f13f2678d875372cf8489ceb7124623a39fa5bf8de8ee1ec722dbb2ec5ec7845",
136 | "https://deno.land/x/deno_graph@0.6.0/lib/types.d.ts": "68cb232e02a984658b40ffaf6cafb979a06fbfdce7f5bd4c7a83ed1a32a07687",
137 | "https://deno.land/x/deno_graph@0.6.0/mod.ts": "8fe3d39bdcb273adfb41a0bafbbaabec4c6fe6c611b47fed8f46f218edb37e8e",
138 | "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts": "91eb1c4bfadfbeda30171007bac6d85aadacd43224a5ed721bbe56bc64e9eb66",
139 | "https://deno.land/x/dnt@0.17.0/lib/compiler.ts": "b741c149adaa3ed87877324ce4365fbb66e8ecbfb5bc417495dcb1ac2936f9ff",
140 | "https://deno.land/x/dnt@0.17.0/lib/compiler_transforms.ts": "b14a398776f08d57a3a011c8c4b5a6a557032c4b125264f86f7877d3f0eef657",
141 | "https://deno.land/x/dnt@0.17.0/lib/mod.deps.ts": "d6c6eb09013c471584bc271261ed896d9f0f6552620ac7d1d65cb46e57841236",
142 | "https://deno.land/x/dnt@0.17.0/lib/npm_ignore.ts": "36fe32008cd71e995bc08569d2b43e8fba816cbada82fa37d1fe52358d5a2e17",
143 | "https://deno.land/x/dnt@0.17.0/lib/package_json.ts": "c8ea56c948fa2379de2a1cf76d335d1b1c70a8c122e1d11932a84f4b70905275",
144 | "https://deno.land/x/dnt@0.17.0/lib/pkg/dnt_wasm.js": "fc7ae54f777566ef7769825491cb868da9f92ec4453326c075ce1b3b2bdf06f3",
145 | "https://deno.land/x/dnt@0.17.0/lib/pkg/dnt_wasm_bg.ts": "51742660365970445ca1e010d340085c7b1188166cc7cdb0d2ba72b3328f276d",
146 | "https://deno.land/x/dnt@0.17.0/lib/pkg/snippets/dnt-wasm-a15ef721fa5290c5/helpers.js": "2f623f83602d4fbb30caa63444b10e35b45e9c2b267e49585ec9bb790a4888d8",
147 | "https://deno.land/x/dnt@0.17.0/lib/shims.ts": "f9f7defa1cadaec4f04f47ac62757ffa47f83b4a5825fe68cbbfcf14086fcce7",
148 | "https://deno.land/x/dnt@0.17.0/lib/test_runner/get_test_runner_code.ts": "5fe5543c8479b5f17c58db4d994de3f3d573e3ca7e4c32c7cf8e338e8e900ba7",
149 | "https://deno.land/x/dnt@0.17.0/lib/test_runner/test_runner.ts": "fc93277907a49bdc098237619da7ff6898547f6f25b71c026375a81efbc8907b",
150 | "https://deno.land/x/dnt@0.17.0/lib/transform.deps.ts": "0b86d5ad77af5b53ad72308fbf5b694abea1f0593652a662be7529ae60c540cb",
151 | "https://deno.land/x/dnt@0.17.0/lib/types.ts": "8506b5ced3921a6ac2a1d5a2bb381bfdbf818c68207f14a1a1fffbf48ee95886",
152 | "https://deno.land/x/dnt@0.17.0/lib/utils.ts": "d2681d634dfa6bd4ad2a32ad15bd419f6f1f895e06c0bf479455fbf1c5f49cd9",
153 | "https://deno.land/x/dnt@0.17.0/mod.ts": "1cd16a64fa1861d624d6e9e02eaa0dfa733f8e2101686fb90e05bbb96ce5221d",
154 | "https://deno.land/x/dnt@0.17.0/transform.ts": "0b1dc1fdf860b594fb50a3aee54bc8c22220f4601f1d227f4113b9b4c0c5166f",
155 | "https://deno.land/x/dnt@0.40.0/lib/compiler.ts": "7f4447531581896348b8a379ab94730856b42ae50d99043f2468328360293cb1",
156 | "https://deno.land/x/dnt@0.40.0/lib/compiler_transforms.ts": "f21aba052f5dcf0b0595c734450842855c7f572e96165d3d34f8fed2fc1f7ba1",
157 | "https://deno.land/x/dnt@0.40.0/lib/mod.deps.ts": "8d6123c8e1162037e58aa8126686a03d1e2cffb250a8757bf715f80242097597",
158 | "https://deno.land/x/dnt@0.40.0/lib/npm_ignore.ts": "57fbb7e7b935417d225eec586c6aa240288905eb095847d3f6a88e290209df4e",
159 | "https://deno.land/x/dnt@0.40.0/lib/package_json.ts": "607b0a4f44acad071a4c8533b312a27d6671eac8e6a23625c8350ce29eadb2ba",
160 | "https://deno.land/x/dnt@0.40.0/lib/pkg/dnt_wasm.generated.js": "2694546844a50861d6d1610859afbf5130baca4dc6cf304541b7ec2d6d998142",
161 | "https://deno.land/x/dnt@0.40.0/lib/pkg/snippets/dnt-wasm-a15ef721fa5290c5/helpers.js": "aba69a019a6da6f084898a6c7b903b8b583bc0dbd82bfb338449cf0b5bce58fd",
162 | "https://deno.land/x/dnt@0.40.0/lib/shims.ts": "39e5c141f0315c0faf30b479b53f92b9078d92e1fd67ee34cc60b701d8e68dab",
163 | "https://deno.land/x/dnt@0.40.0/lib/test_runner/get_test_runner_code.ts": "4dc7a73a13b027341c0688df2b29a4ef102f287c126f134c33f69f0339b46968",
164 | "https://deno.land/x/dnt@0.40.0/lib/test_runner/test_runner.ts": "4d0da0500ec427d5f390d9a8d42fb882fbeccc92c92d66b6f2e758606dbd40e6",
165 | "https://deno.land/x/dnt@0.40.0/lib/transform.deps.ts": "2e159661e1c5c650de9a573babe0e319349fe493105157307ec2ad2f6a52c94e",
166 | "https://deno.land/x/dnt@0.40.0/lib/types.ts": "b8e228b2fac44c2ae902fbb73b1689f6ab889915bd66486c8a85c0c24255f5fb",
167 | "https://deno.land/x/dnt@0.40.0/lib/utils.ts": "224f15f33e7226a2fd991e438d0291d7ed8c7889807efa2e1ecb67d2d1db6720",
168 | "https://deno.land/x/dnt@0.40.0/mod.ts": "ae1890fbe592e4797e7dd88c1e270f22b8334878e9bf187c4e11ae75746fe778",
169 | "https://deno.land/x/dnt@0.40.0/transform.ts": "f68743a14cf9bf53bfc9c81073871d69d447a7f9e3453e0447ca2fb78926bb1d",
170 | "https://deno.land/x/ts_morph@13.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9",
171 | "https://deno.land/x/ts_morph@13.0.0/bootstrap/ts_morph_bootstrap.d.ts": "1dd46307a28a8e689ce214cb0fe260a280e7ecbaceb6fbae3cbcaa8a25a9fd3f",
172 | "https://deno.land/x/ts_morph@13.0.0/bootstrap/ts_morph_bootstrap.js": "5b0c39c5a35d1445bfccfda2a31bf6235e269aacc304b2c6f21ec6fb3c346b26",
173 | "https://deno.land/x/ts_morph@13.0.0/common/DenoRuntime.ts": "b9ac7200ac980c1aea503cf8302e6581c8ddcdc3dbca850d0dd5a43969d7d16e",
174 | "https://deno.land/x/ts_morph@13.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed",
175 | "https://deno.land/x/ts_morph@13.0.0/common/ts_morph_common.d.ts": "32345569d0356af0870227ca7cbc61a25d7f5e9e6b47ec2b3dca267f8f711840",
176 | "https://deno.land/x/ts_morph@13.0.0/common/ts_morph_common.js": "7a63dede12ac30195099f42a07f8ae139799738351ed66e34d872e842d2ca687",
177 | "https://deno.land/x/ts_morph@13.0.0/common/typescript.d.ts": "bbfbfa714a8f9fdf10e9483b4abd82ccb546fc0fc069ad80d0a254fa40930e98",
178 | "https://deno.land/x/ts_morph@13.0.0/common/typescript.js": "dca27ae23ee7eaa46a03e7e7216755186a4c5aff76273e0a66d30358be0e121b",
179 | "https://deno.land/x/ts_morph@20.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9",
180 | "https://deno.land/x/ts_morph@20.0.0/bootstrap/ts_morph_bootstrap.js": "6645ac03c5e6687dfa8c78109dc5df0250b811ecb3aea2d97c504c35e8401c06",
181 | "https://deno.land/x/ts_morph@20.0.0/common/DenoRuntime.ts": "6a7180f0c6e90dcf23ccffc86aa8271c20b1c4f34c570588d08a45880b7e172d",
182 | "https://deno.land/x/ts_morph@20.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed",
183 | "https://deno.land/x/ts_morph@20.0.0/common/ts_morph_common.js": "2325f94f61dc5f3f98a1dab366dc93048d11b1433d718b10cfc6ee5a1cfebe8f",
184 | "https://deno.land/x/ts_morph@20.0.0/common/typescript.js": "b9edf0a451685d13e0467a7ed4351d112b74bd1e256b915a2b941054e31c1736",
185 | "https://deno.land/x/wasmbuild@0.15.1/cache.ts": "9d01b5cb24e7f2a942bbd8d14b093751fa690a6cde8e21709ddc97667e6669ed",
186 | "https://deno.land/x/wasmbuild@0.15.1/loader.ts": "8c2fc10e21678e42f84c5135d8ab6ab7dc92424c3f05d2354896a29ccfd02a63"
187 | },
188 | "workspace": {
189 | "packageJson": {
190 | "dependencies": [
191 | "npm:lerna@^6.0.0"
192 | ]
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "ci": false,
3 | "packages": [
4 | "packages/*"
5 | ],
6 | "version": "0.9.0",
7 | "granularPathspec": false,
8 | "forcePublish": true,
9 | "command": {
10 | "bootstrap": {
11 | "hoist": true
12 | },
13 | "publish": {
14 | "allowBranch": "main",
15 | "message": "chore(release): publish %s",
16 | "conventionalCommits": true,
17 | "createRelease": "github"
18 | }
19 | },
20 | "ignoreChanges": [
21 | "packages/*"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/main/build.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'https://deno.land/std@0.115.1/path/mod.ts';
2 | import { build } from 'https://deno.land/x/dnt@0.40.0/mod.ts';
3 | import hooksPackage from './hooks/package.json.ts';
4 |
5 | const __dirname = new URL('.', import.meta.url).pathname;
6 |
7 | const buildModule = async (name: string) => {
8 | const inDir = path.join(__dirname, name);
9 | const outDir = path.join(__dirname, '..', `packages/${name}`);
10 | const filesToCopy = ['LICENSE', 'README.md'];
11 |
12 | await build({
13 | entryPoints: [path.join(inDir, 'src/index.ts')],
14 | outDir,
15 | test: false,
16 | shims: {},
17 | compilerOptions: {
18 | importHelpers: false
19 | },
20 | package: hooksPackage
21 | });
22 |
23 | filesToCopy.forEach((filename) =>
24 | Deno.copyFileSync(path.join(inDir, filename), path.join(outDir, filename))
25 | );
26 | };
27 |
28 | await buildModule('hooks');
29 |
--------------------------------------------------------------------------------
/main/hooks/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Feathers
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 |
--------------------------------------------------------------------------------
/main/hooks/README.md:
--------------------------------------------------------------------------------
1 | # Hooks
2 |
3 | [](https://github.com/feathersjs/hooks/actions/workflows/deno.yml)
4 | [](https://www.npmjs.com/package/@feathersjs/hooks)
5 |
6 | `@feathersjs/hooks` brings Koa style middleware to any asynchronous JavaScript and TypeScript function or class.
7 |
8 | ## Installation
9 |
10 | ### Node
11 |
12 | ```
13 | npm install @feathersjs/hooks --save
14 | ```
15 |
16 | ```js
17 | const { hooks } = require('@feathersjs/hooks');
18 |
19 | import { hooks } from '@feathersjs/hooks';
20 | ```
21 |
22 | ### Deno
23 |
24 | ```js
25 | import { hooks } from 'https://deno.land/x/hooks/src/index.ts';
26 | ```
27 |
28 | ## Documentation
29 |
30 | See [feathersjs/hooks](https://github.com/feathersjs/hooks/) for the complete documentation.
31 |
32 | ## License
33 |
34 | Copyright (c) 2020
35 |
36 | Licensed under the [MIT license](LICENSE).
37 |
--------------------------------------------------------------------------------
/main/hooks/package.json.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: '@feathersjs/hooks',
3 | version: '0.0.0',
4 | description: 'Async middleware for JavaScript and TypeScript',
5 | homepage: 'https://feathersjs.com',
6 | keywords: [
7 | 'feathers',
8 | 'hooks',
9 | 'hook',
10 | 'async',
11 | 'middleware',
12 | ],
13 | license: 'MIT',
14 | repository: {
15 | type: 'git',
16 | url: 'git://github.com/feathersjs/hooks.git',
17 | },
18 | author: {
19 | name: 'Feathers contributor',
20 | email: 'hello@feathersjs.com',
21 | url: 'https://feathersjs.com',
22 | },
23 | contributors: [],
24 | bugs: {
25 | url: 'https://github.com/feathersjs/hooks/issues',
26 | },
27 | engines: {
28 | node: '>= 14',
29 | },
30 | publishConfig: {
31 | access: 'public',
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/main/hooks/src/base.ts:
--------------------------------------------------------------------------------
1 | import { AsyncMiddleware } from './compose.ts';
2 | import { copyProperties } from './utils.ts';
3 |
4 | export const HOOKS: string = Symbol.for('@feathersjs/hooks') as any;
5 |
6 | export type HookContextData = { [key: string]: any };
7 |
8 | /**
9 | * The base hook context.
10 | */
11 | export class BaseHookContext {
12 | self?: C;
13 | [key: string]: any;
14 |
15 | constructor(data: HookContextData = {}) {
16 | Object.assign(this, data);
17 | }
18 |
19 | toJSON() {
20 | const keys = Object.keys(this);
21 | let proto = Object.getPrototypeOf(this);
22 |
23 | while (proto) {
24 | keys.push(...Object.keys(proto));
25 | proto = Object.getPrototypeOf(proto);
26 | }
27 |
28 | return keys.reduce((result: this, key: string) => {
29 | result[key] = this[key];
30 |
31 | return result;
32 | }, {} as this);
33 | }
34 | }
35 |
36 | export interface HookContext extends BaseHookContext {
37 | result?: T;
38 | method?: string;
39 | arguments: any[];
40 | }
41 |
42 | export type HookContextConstructor = new (data?: {
43 | [key: string]: any;
44 | }) => BaseHookContext;
45 |
46 | export type HookDefaultsInitializer = (
47 | self?: any,
48 | args?: any[],
49 | context?: HookContext
50 | ) => HookContextData;
51 |
52 | export class HookManager {
53 | _parent?: this | null = null;
54 | _params: string[] | null = null;
55 | _middleware: AsyncMiddleware[] | null = null;
56 | _props: HookContextData | null = null;
57 | _defaults?: HookDefaultsInitializer;
58 |
59 | parent(parent: this | null) {
60 | this._parent = parent;
61 |
62 | return this;
63 | }
64 |
65 | middleware(middleware?: AsyncMiddleware[]) {
66 | this._middleware = middleware?.length ? middleware : null;
67 |
68 | return this;
69 | }
70 |
71 | getMiddleware(): AsyncMiddleware[] | null {
72 | const previous = this._parent?.getMiddleware();
73 |
74 | if (previous && this._middleware) {
75 | return previous.concat(this._middleware);
76 | }
77 |
78 | return previous || this._middleware;
79 | }
80 |
81 | collectMiddleware(self: any, _args: any[]): AsyncMiddleware[] {
82 | const otherMiddleware = getMiddleware(self);
83 | const middleware = this.getMiddleware();
84 |
85 | if (otherMiddleware && middleware) {
86 | return otherMiddleware.concat(middleware);
87 | }
88 |
89 | return otherMiddleware || middleware || [];
90 | }
91 |
92 | props(props: HookContextData) {
93 | if (!this._props) {
94 | this._props = {};
95 | }
96 |
97 | copyProperties(this._props, props);
98 |
99 | return this;
100 | }
101 |
102 | getProps(): HookContextData | null {
103 | const previous = this._parent?.getProps();
104 |
105 | if (previous && this._props) {
106 | return copyProperties({}, previous, this._props);
107 | }
108 |
109 | return previous || this._props || null;
110 | }
111 |
112 | params(...params: string[]) {
113 | this._params = params;
114 |
115 | return this;
116 | }
117 |
118 | getParams(): string[] | null {
119 | const previous = this._parent?.getParams();
120 |
121 | if (previous && this._params) {
122 | return previous.concat(this._params);
123 | }
124 |
125 | return previous || this._params;
126 | }
127 |
128 | defaults(defaults: HookDefaultsInitializer) {
129 | this._defaults = defaults;
130 |
131 | return this;
132 | }
133 |
134 | getDefaults(
135 | self: any,
136 | args: any[],
137 | context: HookContext
138 | ): HookContextData | null {
139 | const defaults =
140 | typeof this._defaults === 'function'
141 | ? this._defaults(self, args, context)
142 | : null;
143 | const previous = this._parent?.getDefaults(self, args, context);
144 |
145 | if (previous && defaults) {
146 | return Object.assign({}, previous, defaults);
147 | }
148 |
149 | return previous || defaults;
150 | }
151 |
152 | getContextClass(
153 | Base: HookContextConstructor = BaseHookContext
154 | ): HookContextConstructor {
155 | const ContextClass = class ContextClass extends Base {
156 | constructor(data: any) {
157 | super(data);
158 | }
159 | };
160 | const params = this.getParams();
161 | const props = this.getProps();
162 |
163 | if (params) {
164 | params.forEach((name, index) => {
165 | if (props?.[name] !== undefined) {
166 | throw new Error(
167 | `Hooks can not have a property and param named '${name}'. Use .defaults instead.`
168 | );
169 | }
170 |
171 | Object.defineProperty(ContextClass.prototype, name, {
172 | enumerable: true,
173 | get() {
174 | return this?.arguments[index];
175 | },
176 | set(value: any) {
177 | this.arguments[index] = value;
178 | }
179 | });
180 | });
181 | }
182 |
183 | if (props) {
184 | copyProperties(ContextClass.prototype, props);
185 | }
186 |
187 | return ContextClass;
188 | }
189 |
190 | initializeContext(self: any, args: any[], context: HookContext): HookContext {
191 | const ctx = this._parent
192 | ? this._parent.initializeContext(self, args, context)
193 | : context;
194 | const defaults = this.getDefaults(self, args, ctx);
195 |
196 | if (self) {
197 | ctx.self = self;
198 | }
199 |
200 | ctx.arguments = args;
201 |
202 | if (defaults) {
203 | for (const name of Object.keys(defaults)) {
204 | if (ctx[name] === undefined) {
205 | ctx[name] = defaults[name];
206 | }
207 | }
208 | }
209 |
210 | return ctx;
211 | }
212 | }
213 |
214 | export type HookOptions = HookManager | AsyncMiddleware[] | null;
215 |
216 | export function convertOptions(options: HookOptions = null) {
217 | if (!options) {
218 | return new HookManager();
219 | }
220 |
221 | return Array.isArray(options)
222 | ? new HookManager().middleware(options)
223 | : options;
224 | }
225 |
226 | export function getManager(target: any): HookManager | null {
227 | return (target && target[HOOKS]) || null;
228 | }
229 |
230 | export function setManager(target: T, manager: HookManager) {
231 | const parent = getManager(target);
232 |
233 | (target as any)[HOOKS] = manager.parent(parent);
234 |
235 | return target;
236 | }
237 |
238 | export function getMiddleware(target: any): AsyncMiddleware[] | null {
239 | const manager = getManager(target);
240 |
241 | return manager ? manager.getMiddleware() : null;
242 | }
243 |
244 | export function setMiddleware(target: T, middleware: AsyncMiddleware[]) {
245 | const manager = new HookManager().middleware(middleware);
246 |
247 | return setManager(target, manager);
248 | }
249 |
--------------------------------------------------------------------------------
/main/hooks/src/compose.ts:
--------------------------------------------------------------------------------
1 | // TypeScript port of koa-compose (https://github.com/koajs/compose)
2 | export type NextFunction = () => Promise;
3 |
4 | export type AsyncMiddleware = (
5 | context: T,
6 | next: NextFunction,
7 | ) => Promise;
8 | export type Middleware = AsyncMiddleware;
9 |
10 | export function compose(middleware: AsyncMiddleware[]) {
11 | if (!Array.isArray(middleware)) {
12 | throw new TypeError('Middleware stack must be an array!');
13 | }
14 |
15 | for (const fn of middleware) {
16 | if (typeof fn !== 'function') {
17 | throw new TypeError('Middleware must be composed of functions!');
18 | }
19 | }
20 |
21 | return function (this: any, context: T, next?: AsyncMiddleware) {
22 | // last called middleware #
23 | let index = -1;
24 |
25 | return dispatch.call(this, 0);
26 |
27 | function dispatch(this: any, i: number): Promise {
28 | if (i <= index) {
29 | return Promise.reject(new Error('next() called multiple times'));
30 | }
31 |
32 | index = i;
33 |
34 | let fn: AsyncMiddleware | undefined = middleware[i];
35 |
36 | if (i === middleware.length) {
37 | fn = next;
38 | }
39 |
40 | if (!fn) {
41 | return Promise.resolve();
42 | }
43 |
44 | try {
45 | return Promise.resolve(
46 | fn.call(this, context, dispatch.bind(this, i + 1)),
47 | );
48 | } catch (err) {
49 | return Promise.reject(err);
50 | }
51 | }
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/main/hooks/src/hooks.ts:
--------------------------------------------------------------------------------
1 | import { AsyncMiddleware, compose } from './compose.ts';
2 | import {
3 | convertOptions,
4 | HookContext,
5 | HookContextData,
6 | HookOptions,
7 | setManager,
8 | setMiddleware
9 | } from './base.ts';
10 | import { copyFnProperties, copyProperties } from './utils.ts';
11 |
12 | export function getOriginal(fn: any): any {
13 | return typeof fn.original === 'function' ? getOriginal(fn.original) : fn;
14 | }
15 |
16 | export function functionHooks(fn: F, managerOrMiddleware: HookOptions) {
17 | if (typeof fn !== 'function') {
18 | throw new Error('Can not apply hooks to non-function');
19 | }
20 |
21 | const manager = convertOptions(managerOrMiddleware);
22 | const wrapper: any = function (this: any, ...args: any[]) {
23 | const { Context, original } = wrapper;
24 | // If we got passed an existing HookContext instance, we want to return it as well
25 | const returnContext = args[args.length - 1] instanceof Context;
26 | // Use existing context or default
27 | const base = returnContext ? (args.pop() as HookContext) : new Context();
28 | // Initialize the context
29 | const context = manager.initializeContext(this, args, base);
30 | // Assemble the hook chain
31 | const hookChain: AsyncMiddleware[] = [
32 | // Return `ctx.result` or the context
33 | (ctx, next) => next().then(() => (returnContext ? ctx : ctx.result))
34 | ];
35 |
36 | // Create the hook chain by calling the `collectMiddleware function
37 | const mw = manager.collectMiddleware(this, args);
38 |
39 | if (mw) {
40 | Array.prototype.push.apply(hookChain, mw);
41 | }
42 |
43 | // Runs the actual original method if `ctx.result` is not already set
44 | hookChain.push((ctx, next) => {
45 | if (!Object.prototype.hasOwnProperty.call(context, 'result')) {
46 | return Promise.resolve(original.apply(this, ctx.arguments)).then(
47 | (result) => {
48 | ctx.result = result;
49 |
50 | return next();
51 | }
52 | );
53 | }
54 |
55 | return next();
56 | });
57 |
58 | return compose(hookChain).call(this, context);
59 | };
60 |
61 | copyFnProperties(wrapper, fn);
62 | copyProperties(wrapper, fn);
63 | setManager(wrapper, manager);
64 |
65 | return Object.assign(wrapper, {
66 | original: getOriginal(fn),
67 | Context: manager.getContextClass(),
68 | createContext: (data: HookContextData = {}) => {
69 | return new wrapper.Context(data);
70 | }
71 | });
72 | }
73 |
74 | export type HookMap = {
75 | [L in keyof O]?: HookOptions;
76 | };
77 |
78 | export function objectHooks(obj: any, hooks: HookMap | AsyncMiddleware[]) {
79 | if (Array.isArray(hooks)) {
80 | return setMiddleware(obj, hooks);
81 | }
82 |
83 | for (const method of Object.keys(hooks)) {
84 | const target = typeof obj[method] === 'function' ? obj : obj.prototype;
85 | const fn = target && target[method];
86 |
87 | if (typeof fn !== 'function') {
88 | throw new Error(`Can not apply hooks. '${method}' is not a function`);
89 | }
90 |
91 | const manager = convertOptions(hooks[method]);
92 |
93 | target[method] = functionHooks(fn, manager.props({ method }));
94 | }
95 |
96 | return obj;
97 | }
98 |
99 | export const hookDecorator = (managerOrMiddleware?: HookOptions) => {
100 | return (target: any, context: DecoratorContext) => {
101 | const manager = convertOptions(managerOrMiddleware);
102 |
103 | if (context.kind === 'class') {
104 | setManager(target.prototype, manager);
105 | return target;
106 | } else if (context.kind === 'method') {
107 | const method = String(context.name);
108 | return functionHooks(target, manager.props({ method }));
109 | }
110 |
111 | throw new Error('Can not apply hooks.');
112 | };
113 | };
114 |
115 | export const legacyDecorator = (managerOrMiddleware?: HookOptions) => {
116 | const wrapper: any = (
117 | _target: any,
118 | method: string,
119 | descriptor: TypedPropertyDescriptor
120 | ): TypedPropertyDescriptor => {
121 | const manager = convertOptions(managerOrMiddleware);
122 |
123 | if (!descriptor) {
124 | setManager(_target.prototype, manager);
125 |
126 | return _target;
127 | }
128 |
129 | const fn = descriptor.value;
130 |
131 | if (typeof fn !== 'function') {
132 | throw new Error(`Can not apply hooks. '${method}' is not a function`);
133 | }
134 |
135 | descriptor.value = functionHooks(fn, manager.props({ method }));
136 |
137 | return descriptor;
138 | };
139 |
140 | return wrapper;
141 | };
142 |
--------------------------------------------------------------------------------
/main/hooks/src/index.ts:
--------------------------------------------------------------------------------
1 | import { AsyncMiddleware } from './compose.ts';
2 | import { HookContext, HookContextConstructor, HookContextData, HookManager, HookOptions } from './base.ts';
3 | import { functionHooks, hookDecorator, HookMap, objectHooks } from './hooks.ts';
4 |
5 | export * from './hooks.ts';
6 | export * from './compose.ts';
7 | export * from './base.ts';
8 | export * from './regular.ts';
9 |
10 | export interface WrapperAddon {
11 | original: F;
12 | Context: HookContextConstructor;
13 | createContext: (data?: HookContextData) => HookContext;
14 | }
15 |
16 | export type WrappedFunction =
17 | & F
18 | & ((...rest: any[]) => Promise | Promise)
19 | & WrapperAddon;
20 |
21 | export type MiddlewareOptions = {
22 | params?: any;
23 | defaults?: any;
24 | props?: any;
25 | };
26 |
27 | /**
28 | * Initializes a hook settings object with the given middleware.
29 | * @param mw The list of middleware
30 | * @param options Middleware options (params, default, props)
31 | */
32 | export function middleware(
33 | mw?: AsyncMiddleware[],
34 | options?: MiddlewareOptions,
35 | ) {
36 | const manager = new HookManager().middleware(mw);
37 |
38 | if (options) {
39 | if (options.params) {
40 | manager.params(...options.params);
41 | }
42 |
43 | if (options.defaults) {
44 | manager.defaults(options.defaults);
45 | }
46 |
47 | if (options.props) {
48 | manager.props(options.props);
49 | }
50 | }
51 |
52 | return manager;
53 | }
54 |
55 | /**
56 | * Returns a new function that wraps an existing async function
57 | * with hooks.
58 | *
59 | * @param fn The async function to add hooks to.
60 | * @param manager An array of middleware or hook settings
61 | * (`middleware([]).params()` etc.)
62 | */
63 | export function hooks(
64 | fn: F & (() => void),
65 | manager?: HookManager,
66 | ): WrappedFunction;
67 |
68 | /**
69 | * Add hooks to one or more methods on an object or class.
70 | * @param obj The object to add hooks to
71 | * @param hookMap A map of middleware settings where the
72 | * key is the method name.
73 | */
74 | export function hooks(
75 | obj: O | (new (...args: any[]) => O),
76 | hookMap: HookMap | AsyncMiddleware[],
77 | ): O;
78 |
79 | /**
80 | * Decorate a class method with hooks.
81 | * @param manager The hooks settings
82 | */
83 | export function hooks(
84 | manager?: HookOptions,
85 | ): any;
86 |
87 | // Fallthrough to actual implementation
88 | export function hooks(...args: any[]) {
89 | const [target, _hooks] = args;
90 |
91 | if (
92 | typeof target === 'function' &&
93 | (_hooks instanceof HookManager || Array.isArray(_hooks) ||
94 | args.length === 1)
95 | ) {
96 | return functionHooks(target, _hooks);
97 | }
98 |
99 | if (args.length === 2) {
100 | return objectHooks(target, _hooks);
101 | }
102 |
103 | return hookDecorator(target);
104 | }
105 |
--------------------------------------------------------------------------------
/main/hooks/src/regular.ts:
--------------------------------------------------------------------------------
1 | import { compose } from './compose.ts';
2 | import { HookContext } from './base.ts';
3 |
4 | export type RegularMiddleware = (
5 | context: T,
6 | ) => Promise | any;
7 | export interface RegularHookMap {
8 | before?: RegularMiddleware[];
9 | after?: RegularMiddleware[];
10 | error?: RegularMiddleware[];
11 | }
12 |
13 | export const runHook = (
14 | hook: RegularMiddleware,
15 | context: any,
16 | type?: string,
17 | ) => {
18 | const typeBefore = context.type;
19 | if (type) context.type = type;
20 | return Promise.resolve(hook.call(context.self, context)).then((res: any) => {
21 | if (type) context.type = typeBefore;
22 | if (res && res !== context) {
23 | Object.assign(context, res);
24 | }
25 | });
26 | };
27 |
28 | export const runHooks = (hooks: RegularMiddleware[]) => (context: any) =>
29 | hooks.reduce(
30 | (promise, hook) => promise.then(() => runHook(hook, context)),
31 | Promise.resolve(context),
32 | );
33 |
34 | export function fromBeforeHook(hook: RegularMiddleware) {
35 | return (context: any, next: any) => {
36 | return runHook(hook, context, 'before').then(next);
37 | };
38 | }
39 |
40 | export function fromAfterHook(hook: RegularMiddleware) {
41 | return (context: any, next: any) => {
42 | return next().then(() => runHook(hook, context, 'after'));
43 | };
44 | }
45 |
46 | export function fromErrorHook(hook: RegularMiddleware) {
47 | return (context: any, next: any) => {
48 | return next().catch((error: any) => {
49 | if (context.error !== error || context.result !== undefined) {
50 | (context as any).original = { ...context };
51 | context.error = error;
52 | delete context.result;
53 | }
54 |
55 | return runHook(hook, context, 'error')
56 | .then(() => {
57 | if (context.result === undefined && context.error !== undefined) {
58 | throw context.error;
59 | }
60 | })
61 | .catch((error) => {
62 | context.error = error;
63 | throw context.error;
64 | });
65 | });
66 | };
67 | }
68 |
69 | export function collect({
70 | before = [],
71 | after = [],
72 | error = [],
73 | }: RegularHookMap) {
74 | const beforeHooks = before.map(fromBeforeHook);
75 | const afterHooks = [...after].reverse().map(fromAfterHook);
76 | const errorHooks = error.length ? [fromErrorHook(runHooks(error))] : [];
77 |
78 | return compose([...errorHooks, ...beforeHooks, ...afterHooks]);
79 | }
80 |
--------------------------------------------------------------------------------
/main/hooks/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function copyProperties(target: F, ...originals: any[]) {
2 | for (const original of originals) {
3 | const originalProps = (Object.keys(original) as any).concat(
4 | Object.getOwnPropertySymbols(original),
5 | );
6 |
7 | for (const prop of originalProps) {
8 | const propDescriptor = Object.getOwnPropertyDescriptor(original, prop);
9 |
10 | if (
11 | propDescriptor &&
12 | !Object.prototype.hasOwnProperty.call(target, prop)
13 | ) {
14 | Object.defineProperty(target, prop, propDescriptor);
15 | }
16 | }
17 | }
18 |
19 | return target;
20 | }
21 |
22 | export function copyFnProperties(target: F, original: any) {
23 | const internalProps = ['name', 'length'];
24 |
25 | try {
26 | for (const prop of internalProps) {
27 | const value = original[prop];
28 |
29 | Object.defineProperty(target, prop, { value });
30 | }
31 | } catch (_e) {
32 | // Avoid IE error
33 | }
34 |
35 | return target;
36 | }
37 |
--------------------------------------------------------------------------------
/main/hooks/test/benchmark.test.ts:
--------------------------------------------------------------------------------
1 | import { assert, it } from './dependencies.ts';
2 | import { HookContext, hooks, middleware, NextFunction } from '../src/index.ts';
3 |
4 | const CYCLES = 100000;
5 | const getRuntime = async (callback: () => Promise) => {
6 | const start = Date.now();
7 |
8 | for (let i = 0; i < CYCLES; i++) {
9 | await callback();
10 | }
11 |
12 | return Date.now() - start;
13 | };
14 |
15 | const hello = async (name: string, _params: any = {}) => {
16 | return `Hello ${name}`;
17 | };
18 | let baseline: number;
19 | let threshold: number;
20 |
21 | (async () => {
22 | baseline = await getRuntime(() => hello('Dave'));
23 | threshold = baseline * 10;
24 | })();
25 |
26 | it('empty hook', async () => {
27 | const hookHello1 = hooks(hello, middleware([]));
28 | const runtime = await getRuntime(() => hookHello1('Dave'));
29 |
30 | assert(
31 | runtime < threshold,
32 | `Runtime is ${runtime}ms, threshold is ${threshold}ms`,
33 | );
34 | });
35 |
36 | it('single simple hook', async () => {
37 | const hookHello = hooks(
38 | hello,
39 | middleware([
40 | async (_ctx: HookContext, next: NextFunction) => {
41 | await next();
42 | },
43 | ]),
44 | );
45 | const runtime = await getRuntime(() => hookHello('Dave'));
46 |
47 | assert(
48 | runtime < threshold,
49 | `Runtime is ${runtime}ms, threshold is ${threshold}ms`,
50 | );
51 | });
52 |
53 | it('single hook, withParams and props', async () => {
54 | const hookHello = hooks(
55 | hello,
56 | middleware([
57 | async (_ctx: HookContext, next: NextFunction) => {
58 | await next();
59 | },
60 | ])
61 | .params('name')
62 | .props({ dave: true }),
63 | );
64 |
65 | const runtime = await getRuntime(() => hookHello('Dave'));
66 |
67 | assert(
68 | runtime < threshold,
69 | `Runtime is ${runtime}ms, threshold is ${threshold}ms`,
70 | );
71 | });
72 |
--------------------------------------------------------------------------------
/main/hooks/test/class.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertStrictEquals, it } from './dependencies.ts';
2 | import { HookContext, hooks, middleware, NextFunction } from '../src/index.ts';
3 |
4 | interface Dummy {
5 | sayHi(name: string): Promise;
6 | addOne(number: number): Promise;
7 | }
8 |
9 | const createDummyClass = () => {
10 | return class DummyClass implements Dummy {
11 | sayHi(name: string) {
12 | return Promise.resolve(`Hi ${name}`);
13 | }
14 |
15 | addOne(number: number) {
16 | return Promise.resolve(number + 1);
17 | }
18 | };
19 | };
20 |
21 | it('hooking object on class adds to the prototype', async () => {
22 | const DummyClass = createDummyClass();
23 |
24 | hooks(DummyClass, {
25 | sayHi: middleware([
26 | async (ctx: HookContext, next: NextFunction) => {
27 | assertEquals(ctx.toJSON(), {
28 | arguments: ['David'],
29 | method: 'sayHi',
30 | name: 'David',
31 | self: instance,
32 | });
33 |
34 | await next();
35 |
36 | ctx.result += '?';
37 | },
38 | ]).params('name'),
39 |
40 | addOne: middleware([
41 | async (ctx: HookContext, next: NextFunction) => {
42 | ctx.arguments[0] += 1;
43 |
44 | await next();
45 | },
46 | ]),
47 | });
48 |
49 | const instance = new DummyClass();
50 |
51 | assertEquals(await instance.sayHi('David'), 'Hi David?');
52 | assertEquals(await instance.addOne(1), 3);
53 | });
54 |
55 | it('hooking object works on function that has property', async () => {
56 | const app = function () {};
57 |
58 | app.sayHi = (name: string) => `Hello ${name}`;
59 |
60 | hooks(app as any, {
61 | sayHi: middleware([
62 | async (ctx: HookContext, next: NextFunction) => {
63 | await next();
64 |
65 | ctx.result += '?';
66 | },
67 | ]).params('name'),
68 | });
69 |
70 | assertEquals(await app.sayHi('David'), 'Hello David?');
71 | });
72 |
73 | it('works with inheritance', async () => {
74 | const DummyClass = createDummyClass();
75 |
76 | const first = async (ctx: HookContext, next: NextFunction) => {
77 | assertEquals(ctx.arguments, ['David']);
78 | assertEquals(ctx.method, 'sayHi');
79 | assertEquals(ctx.self, instance);
80 |
81 | await next();
82 |
83 | ctx.result += '?';
84 | };
85 | const second = async (ctx: HookContext, next: NextFunction) => {
86 | await next();
87 |
88 | ctx.result += '!';
89 | };
90 |
91 | hooks(DummyClass, {
92 | sayHi: middleware([first]),
93 | });
94 |
95 | class OtherDummy extends DummyClass {}
96 |
97 | hooks(OtherDummy, {
98 | sayHi: middleware([second]),
99 | });
100 |
101 | const instance = new OtherDummy();
102 |
103 | assertStrictEquals(await instance.sayHi('David'), 'Hi David!?');
104 | });
105 |
106 | it('works with multiple context updaters', async () => {
107 | const DummyClass = createDummyClass();
108 |
109 | hooks(DummyClass, {
110 | sayHi: middleware([
111 | async (ctx, next) => {
112 | assertEquals(ctx.name, 'Dave');
113 |
114 | ctx.name = 'Changed';
115 |
116 | await next();
117 | },
118 | ]).params('name'),
119 | });
120 |
121 | class OtherDummy extends DummyClass {}
122 |
123 | hooks(OtherDummy, {
124 | sayHi: middleware([
125 | async (ctx, next) => {
126 | assertEquals(ctx.name, 'Changed');
127 | assertEquals(ctx.gna, 42);
128 |
129 | await next();
130 | },
131 | ]).props({ gna: 42 }),
132 | });
133 |
134 | const instance = new OtherDummy();
135 |
136 | hooks(instance, {
137 | sayHi: middleware([
138 | async (ctx, next) => {
139 | assertEquals(ctx.name, 'Changed');
140 | assertEquals(ctx.gna, 42);
141 | assertEquals(ctx.app, 'ok');
142 |
143 | await next();
144 | },
145 | ]).props({ app: 'ok' }),
146 | });
147 |
148 | assertEquals(await instance.sayHi('Dave'), 'Hi Changed');
149 | });
150 |
--------------------------------------------------------------------------------
/main/hooks/test/collect.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertStrictEquals, assertThrowsAsync, it } from './dependencies.ts';
2 | import { collect, HookContext, hooks, middleware, NextFunction } from '../src/index.ts';
3 |
4 | it('collect: hooks run in order', async () => {
5 | class DummyClass {
6 | async create(data: any) {
7 | data.id = 1;
8 | return data;
9 | }
10 | }
11 | hooks(DummyClass, {
12 | create: middleware([
13 | collect({
14 | before: [
15 | (ctx: any) => {
16 | ctx.data.log.push('collect-1 : before : 1');
17 | },
18 | (ctx: any) => {
19 | ctx.data.log.push('collect-1 : before : 2');
20 | },
21 | ],
22 | after: [
23 | (ctx: any) => {
24 | ctx.data.log.push('collect-1 : after : 1');
25 | },
26 | (ctx: any) => {
27 | ctx.data.log.push('collect-1 : after : 2');
28 | },
29 | ],
30 | error: [],
31 | }),
32 | async (ctx: HookContext, next: NextFunction) => {
33 | ctx.data.log.push('async : before');
34 | await next();
35 | ctx.data.log.push('async : after');
36 | },
37 | collect({
38 | before: [
39 | (ctx: any) => {
40 | ctx.data.log.push('collect-2 : before : 3');
41 | },
42 | (ctx: any) => {
43 | ctx.data.log.push('collect-2 : before : 4');
44 | },
45 | ],
46 | after: [
47 | (ctx: any) => {
48 | ctx.data.log.push('collect-2 : after : 3');
49 | },
50 | (ctx: any) => {
51 | ctx.data.log.push('collect-2 : after : 4');
52 | },
53 | ],
54 | error: [],
55 | }),
56 | ]).params('data'),
57 | });
58 |
59 | const service = new DummyClass();
60 | const value = await service.create({ name: 'David', log: [] });
61 |
62 | assertEquals(value.log, [
63 | 'collect-1 : before : 1',
64 | 'collect-1 : before : 2',
65 | 'async : before',
66 | 'collect-2 : before : 3',
67 | 'collect-2 : before : 4',
68 | 'collect-2 : after : 3',
69 | 'collect-2 : after : 4',
70 | 'async : after',
71 | 'collect-1 : after : 1',
72 | 'collect-1 : after : 2',
73 | ]);
74 | });
75 |
76 | it('collect: error hooks', async () => {
77 | class DummyClass {
78 | async create(name: string) {
79 | if (name !== 'after') {
80 | throw new Error(`Error in method with ${name}`);
81 | }
82 | }
83 | }
84 |
85 | const collection = collect({
86 | before: [
87 | (ctx) => {
88 | if (ctx.arguments[0] === 'before') {
89 | throw new Error('in before hook');
90 | }
91 | },
92 | ],
93 | after: [
94 | (ctx) => {
95 | if (ctx.arguments[0] === 'after') {
96 | throw new Error('in after hook');
97 | }
98 | },
99 | ],
100 | error: [
101 | (ctx) => {
102 | if (ctx.arguments[0] === 'error') {
103 | throw new Error('in error hook');
104 | }
105 |
106 | if (ctx.arguments[0] === 'result') {
107 | ctx.result = 'result from error hook';
108 | }
109 | },
110 | (ctx) => {
111 | if (ctx.result === 'result from error hook') {
112 | ctx.result += '!';
113 | }
114 | },
115 | ],
116 | });
117 |
118 | hooks(DummyClass, {
119 | create: middleware([collection]).params('data'),
120 | });
121 |
122 | const service = new DummyClass();
123 |
124 | await assertThrowsAsync(
125 | () => service.create('test'),
126 | undefined,
127 | 'Error in method with test',
128 | );
129 |
130 | await assertThrowsAsync(
131 | () => service.create('before'),
132 | undefined,
133 | 'in before hook',
134 | );
135 |
136 | await assertThrowsAsync(
137 | () => service.create('after'),
138 | undefined,
139 | 'in after hook',
140 | );
141 |
142 | await assertThrowsAsync(
143 | () => service.create('error'),
144 | undefined,
145 | 'in error hook',
146 | );
147 |
148 | assertStrictEquals(await service.create('result'), 'result from error hook!');
149 | });
150 |
--------------------------------------------------------------------------------
/main/hooks/test/compose.test.ts:
--------------------------------------------------------------------------------
1 | // Adapted from koa-compose (https://github.com/koajs/compose)
2 | import { assert, assertEquals, assertStrictEquals, assertThrows, it } from './dependencies.ts';
3 | import { compose, NextFunction } from '../src/index.ts';
4 |
5 | function wait(ms: number) {
6 | return new Promise((resolve) => setTimeout(resolve, ms || 1));
7 | }
8 |
9 | function isPromise(x: any) {
10 | return x && typeof x.then === 'function';
11 | }
12 |
13 | it('compose: should work', async () => {
14 | const arr: number[] = [];
15 | const stack = [];
16 |
17 | stack.push(async (_context: any, next: NextFunction) => {
18 | arr.push(1);
19 | await wait(1);
20 | await next();
21 | await wait(1);
22 | arr.push(6);
23 | });
24 |
25 | stack.push(async (_context: any, next: NextFunction) => {
26 | arr.push(2);
27 | await wait(1);
28 | await next();
29 | await wait(1);
30 | arr.push(5);
31 | });
32 |
33 | stack.push(async (_context: any, next: NextFunction) => {
34 | arr.push(3);
35 | await wait(1);
36 | await next();
37 | await wait(1);
38 | arr.push(4);
39 | });
40 |
41 | await compose(stack)({});
42 |
43 | assertEquals(arr, [1, 2, 3, 4, 5, 6]);
44 | });
45 |
46 | it('compose: should be able to be called twice', () => {
47 | const stack = [];
48 |
49 | stack.push(async (context: any, next: NextFunction) => {
50 | context.arr.push(1);
51 | await wait(1);
52 | await next();
53 | await wait(1);
54 | context.arr.push(6);
55 | });
56 |
57 | stack.push(async (context: any, next: NextFunction) => {
58 | context.arr.push(2);
59 | await wait(1);
60 | await next();
61 | await wait(1);
62 | context.arr.push(5);
63 | });
64 |
65 | stack.push(async (context: any, next: NextFunction) => {
66 | context.arr.push(3);
67 | await wait(1);
68 | await next();
69 | await wait(1);
70 | context.arr.push(4);
71 | });
72 |
73 | const fn = compose(stack);
74 | const ctx1: any = { arr: [] };
75 | const ctx2: any = { arr: [] };
76 | const out = [1, 2, 3, 4, 5, 6];
77 |
78 | return fn(ctx1).then(() => {
79 | assertEquals(out, ctx1.arr);
80 | return fn(ctx2);
81 | }).then(() => {
82 | assertEquals(out, ctx2.arr);
83 | });
84 | });
85 |
86 | it('compose: should only accept an array', async () => {
87 | await assertThrows(
88 | // @ts-ignore test without args
89 | () => compose(),
90 | undefined,
91 | 'Middleware stack must be an array!',
92 | );
93 | });
94 |
95 | it('compose: should create next functions that return a Promise', function () {
96 | const stack = [];
97 | const arr: any = [];
98 | for (let i = 0; i < 5; i++) {
99 | stack.push(async (_context: any, next: NextFunction) => {
100 | arr.push(next());
101 | });
102 | }
103 |
104 | compose(stack)({});
105 |
106 | for (const next of arr) {
107 | assert(isPromise(next), 'one of the functions next is not a Promise');
108 | }
109 | });
110 |
111 | it('compose: should work with 0 middleware', function () {
112 | return compose([])({});
113 | });
114 |
115 | it('compose: should only accept middleware as functions', () => {
116 | assertThrows(
117 | () => compose([{}] as any),
118 | undefined,
119 | 'Middleware must be composed of functions!',
120 | );
121 | });
122 |
123 | it('compose: should work when yielding at the end of the stack', async () => {
124 | const stack = [];
125 | let called = false;
126 |
127 | stack.push(async (_ctx: any, next: NextFunction) => {
128 | await next();
129 | called = true;
130 | });
131 |
132 | await compose(stack)({});
133 | assert(called);
134 | });
135 |
136 | it('compose: should reject on errors in middleware', () => {
137 | const stack = [];
138 |
139 | stack.push(() => {
140 | throw new Error();
141 | });
142 |
143 | return compose(stack)({})
144 | .then(function () {
145 | throw new Error('promise was not rejected');
146 | })
147 | .catch(function (e) {
148 | assert(e instanceof Error);
149 | });
150 | });
151 |
152 | it('compose: should keep the context', () => {
153 | const ctx = {};
154 |
155 | const stack = [];
156 |
157 | stack.push(async (ctx2: any, next: NextFunction) => {
158 | await next();
159 | assertEquals(ctx2, ctx);
160 | });
161 |
162 | stack.push(async (ctx2: any, next: NextFunction) => {
163 | await next();
164 | assertEquals(ctx2, ctx);
165 | });
166 |
167 | stack.push(async (ctx2: any, next: NextFunction) => {
168 | await next();
169 | assertEquals(ctx2, ctx);
170 | });
171 |
172 | return compose(stack)(ctx);
173 | });
174 |
175 | it('compose: should catch downstream errors', async () => {
176 | const arr: number[] = [];
177 | const stack = [];
178 |
179 | stack.push(async (_ctx: any, next: NextFunction) => {
180 | arr.push(1);
181 | try {
182 | arr.push(6);
183 | await next();
184 | arr.push(7);
185 | } catch (_err) {
186 | arr.push(2);
187 | }
188 | arr.push(3);
189 | });
190 |
191 | stack.push(async (_ctx: any, _next: NextFunction) => {
192 | arr.push(4);
193 | throw new Error();
194 | });
195 |
196 | await compose(stack)({});
197 | assertEquals(arr, [1, 6, 4, 2, 3]);
198 | });
199 |
200 | it('compose: should compose w/ next', () => {
201 | let called = false;
202 |
203 | return compose([])({}, async () => {
204 | called = true;
205 | }).then(function () {
206 | assert(called);
207 | });
208 | });
209 |
210 | it('compose: should handle errors in wrapped non-async functions', () => {
211 | const stack = [];
212 |
213 | stack.push(function () {
214 | throw new Error();
215 | });
216 |
217 | return compose(stack)({}).then(function () {
218 | throw new Error('promise was not rejected');
219 | }).catch(function (e) {
220 | assert(e instanceof Error);
221 | });
222 | });
223 |
224 | // https://github.com/koajs/compose/pull/27#issuecomment-143109739
225 | it('compose: should compose w/ other compositions', () => {
226 | const called: number[] = [];
227 |
228 | return compose([
229 | compose([
230 | (_ctx, next) => {
231 | called.push(1);
232 | return next();
233 | },
234 | (_ctx, next) => {
235 | called.push(2);
236 | return next();
237 | },
238 | ]),
239 | (_ctx, next) => {
240 | called.push(3);
241 | return next();
242 | },
243 | ])({}).then(() => assertEquals(called, [1, 2, 3]));
244 | });
245 |
246 | it('compose: should throw if next() is called multiple times', () => {
247 | return compose([
248 | async (_ctx, next) => {
249 | await next();
250 | await next();
251 | },
252 | ])({}).then(() => {
253 | throw new Error('boom');
254 | }, (err) => {
255 | assert(/multiple times/.test(err.message));
256 | });
257 | });
258 |
259 | it('compose: should return a valid middleware', () => {
260 | let val = 0;
261 | return compose([
262 | compose([
263 | (_ctx, next) => {
264 | val++;
265 | return next();
266 | },
267 | (_ctx, next) => {
268 | val++;
269 | return next();
270 | },
271 | ]),
272 | (_ctx, next) => {
273 | val++;
274 | return next();
275 | },
276 | ])({}).then(function () {
277 | assertStrictEquals(val, 3);
278 | });
279 | });
280 |
281 | it('compose: should return last return value', () => {
282 | const stack = [];
283 |
284 | stack.push(async (_context: any, next: NextFunction) => {
285 | const val = await next();
286 | assertEquals(val, 2);
287 | return 1;
288 | });
289 |
290 | stack.push(async (_context: any, next: NextFunction) => {
291 | const val = await next();
292 | assertEquals(val, 0);
293 | return 2;
294 | });
295 |
296 | const next = async () => 0;
297 |
298 | return compose(stack)({}, next).then(function (val) {
299 | assertEquals(val, 1);
300 | });
301 | });
302 |
303 | it('compose: should not affect the original middleware array', () => {
304 | const middleware = [];
305 | const fn1 = (_ctx: any, next: NextFunction) => {
306 | return next();
307 | };
308 | middleware.push(fn1);
309 |
310 | for (const fn of middleware) {
311 | assertStrictEquals(fn, fn1);
312 | }
313 |
314 | compose(middleware);
315 |
316 | for (const fn of middleware) {
317 | assertStrictEquals(fn, fn1);
318 | }
319 | });
320 |
321 | it('compose: should not get stuck on the passed in next', () => {
322 | const middleware = [(_ctx: any, next: NextFunction) => {
323 | ctx.middleware++;
324 | return next();
325 | }];
326 | const ctx = {
327 | middleware: 0,
328 | next: 0,
329 | };
330 |
331 | return compose(middleware)(ctx, async (ctx: any, next: NextFunction) => {
332 | ctx.next++;
333 | return next();
334 | }).then(() => {
335 | assertEquals(ctx, { middleware: 1, next: 1 });
336 | });
337 | });
338 |
--------------------------------------------------------------------------------
/main/hooks/test/decorator.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertThrows, it } from './dependencies.ts';
2 | import { HookContext, hooks, middleware, NextFunction } from '../src/index.ts';
3 |
4 | it('hook decorator on method and classes with inheritance', async () => {
5 | const expectedName = 'David NameFromTopLevel NameFromDummyClass';
6 |
7 | @hooks([
8 | async (ctx, next) => {
9 | ctx.arguments[0] += ' NameFromTopLevel';
10 |
11 | await next();
12 |
13 | ctx.result += ' ResultFromTopLevel';
14 | },
15 | ])
16 | class TopLevel {}
17 |
18 | @hooks([
19 | async (ctx, next) => {
20 | ctx.arguments[0] += ' NameFromDummyClass';
21 |
22 | await next();
23 |
24 | ctx.result += ' ResultFromDummyClass';
25 | },
26 | ])
27 | class DummyClass extends TopLevel {
28 | @hooks(
29 | middleware([
30 | async (ctx: HookContext, next: NextFunction) => {
31 | assertEquals(ctx.method, 'sayHi');
32 | assertEquals(ctx.arguments, [expectedName]);
33 | assertEquals(ctx.name, expectedName);
34 |
35 | await next();
36 |
37 | ctx.result += ' ResultFromMethodDecorator';
38 | },
39 | ]).params('name'),
40 | )
41 | async sayHi(name: string) {
42 | return `Hi ${name}`;
43 | }
44 |
45 | @hooks()
46 | async hookedFn() {
47 | return 'Hooks with nothing';
48 | }
49 |
50 | @hooks([async (_ctx: HookContext, next: NextFunction) => next()])
51 | async sayWorld() {
52 | return 'World';
53 | }
54 | }
55 |
56 | const instance = new DummyClass();
57 |
58 | assertEquals(
59 | await instance.sayHi('David'),
60 | `Hi ${expectedName} ResultFromMethodDecorator ResultFromDummyClass ResultFromTopLevel`,
61 | );
62 | });
63 |
64 | it('error cases', () => {
65 | assertThrows(
66 | () => hooks([])({}, 'test', { value: 'not a function' }),
67 | undefined,
68 | `Can not apply hooks.`,
69 | );
70 | });
71 |
--------------------------------------------------------------------------------
/main/hooks/test/dependencies.ts:
--------------------------------------------------------------------------------
1 | export * from 'https://deno.land/std@0.114.0/testing/asserts.ts';
2 |
3 | export const it = (name: string, fn: () => any, only = false) =>
4 | Deno.test({
5 | only,
6 | name,
7 | fn,
8 | });
9 |
10 | it.only = (name: string, fn: () => any) => it(name, fn, true);
11 |
--------------------------------------------------------------------------------
/main/hooks/test/function.test.ts:
--------------------------------------------------------------------------------
1 | import { assert, assertEquals, assertNotStrictEquals, assertStrictEquals, assertThrows, it } from './dependencies.ts';
2 | import {
3 | BaseHookContext,
4 | functionHooks,
5 | getManager,
6 | HookContext,
7 | hooks,
8 | middleware,
9 | NextFunction,
10 | setMiddleware,
11 | } from '../src/index.ts';
12 |
13 | const hello = (name?: string, _params: any = {}) => {
14 | return Promise.resolve(`Hello ${name}`);
15 | };
16 |
17 | it('returns a new function, registers hooks', () => {
18 | const fn = hooks(hello, []);
19 |
20 | assertNotStrictEquals(fn, hello);
21 | assertNotStrictEquals(getManager(fn), null);
22 | });
23 |
24 | it('returns a new function, without hooks', () => {
25 | const fn = hooks(hello);
26 |
27 | assertNotStrictEquals(fn, hello);
28 | assert(getManager(fn) !== null);
29 | });
30 |
31 | it('conserve name and length properties', () => {
32 | const fn = hooks(hello, []);
33 |
34 | assertStrictEquals(fn.length, hello.length);
35 | assertStrictEquals(fn.name, hello.name);
36 | });
37 |
38 | it('throws an error with non function', () => {
39 | assertThrows(() => functionHooks({}, middleware([])));
40 | });
41 |
42 | it('can override arguments, has context', async () => {
43 | const addYou = async (ctx: HookContext, next: NextFunction) => {
44 | assert(ctx instanceof BaseHookContext);
45 | assertEquals(ctx.arguments, ['There']);
46 | ctx.arguments[0] += ' You';
47 |
48 | await next();
49 | };
50 |
51 | const fn = hooks(hello, middleware([addYou]));
52 | const res = await fn('There');
53 |
54 | assertStrictEquals(res, 'Hello There You');
55 | });
56 |
57 | it('has fn.original', async () => {
58 | const fn = hooks(
59 | hello,
60 | middleware([
61 | async (ctx: HookContext, next: NextFunction) => {
62 | ctx.arguments[0] += ' You';
63 |
64 | await next();
65 | },
66 | ]),
67 | );
68 |
69 | assertStrictEquals(typeof fn.original, 'function');
70 |
71 | assertStrictEquals(await fn.original('Dave'), 'Hello Dave');
72 | });
73 |
74 | it('can override context.result before, skips method call', async () => {
75 | const hello = async (_name: string) => {
76 | throw new Error('Should never get here');
77 | };
78 | const updateResult = async (ctx: HookContext, next: NextFunction) => {
79 | ctx.result = 'Hello Dave';
80 |
81 | await next();
82 | };
83 |
84 | const fn = hooks(hello, middleware([updateResult]));
85 | const res = await fn('There');
86 |
87 | assertStrictEquals(res, 'Hello Dave');
88 | });
89 |
90 | it('can set context.result to undefined, skips method call, returns undefined', async () => {
91 | const hello = async (_name: string) => {
92 | throw new Error('Should never get here');
93 | };
94 | const updateResult = async (ctx: HookContext, next: NextFunction) => {
95 | ctx.result = undefined;
96 |
97 | await next();
98 | };
99 |
100 | const fn = hooks(hello, middleware([updateResult]));
101 | const res = await fn('There');
102 |
103 | assertStrictEquals(res, undefined);
104 | });
105 |
106 | it('deleting context.result, does not skip method call', async () => {
107 | const hello = async (name: string) => {
108 | return name;
109 | };
110 | const updateResult = async (ctx: HookContext, next: NextFunction) => {
111 | ctx.result = 'Dave';
112 |
113 | await next();
114 | };
115 | const deleteResult = async (ctx: HookContext, next: NextFunction) => {
116 | delete ctx.result;
117 |
118 | await next();
119 | };
120 |
121 | const fn = hooks(hello, middleware([updateResult, deleteResult]));
122 | const res = await fn('There');
123 |
124 | assertStrictEquals(res, 'There');
125 | });
126 |
127 | it('can override context.result after', async () => {
128 | const updateResult = async (ctx: HookContext, next: NextFunction) => {
129 | await next();
130 |
131 | ctx.result += ' You!';
132 | };
133 |
134 | const fn = hooks(hello, middleware([updateResult]));
135 | const res = await fn('There');
136 |
137 | assertStrictEquals(res, 'Hello There You!');
138 | });
139 |
140 | it('maintains the function context and sets context.self', async () => {
141 | const hook = async function (
142 | this: any,
143 | context: HookContext,
144 | next: NextFunction,
145 | ) {
146 | assertStrictEquals(obj, this);
147 | assertStrictEquals(context.self, obj);
148 | await next();
149 | };
150 | const obj: any = {
151 | message: 'Hi',
152 |
153 | sayHi: hooks(async function (this: any, name: string) {
154 | return `${this.message} ${name}`;
155 | }, middleware([hook])),
156 | };
157 | const res = await obj.sayHi('Dave');
158 |
159 | assertStrictEquals(res, 'Hi Dave');
160 | });
161 |
162 | it('uses hooks from context object and its prototypes', async () => {
163 | const o1 = { message: 'Hi' };
164 | const o2 = Object.create(o1);
165 |
166 | setMiddleware(o1, [
167 | async (ctx: HookContext, next: NextFunction) => {
168 | ctx.arguments[0] += ' o1';
169 |
170 | await next();
171 | },
172 | ]);
173 |
174 | setMiddleware(o2, [
175 | async (ctx, next) => {
176 | ctx.arguments[0] += ' o2';
177 |
178 | await next();
179 | },
180 | ]);
181 |
182 | o2.sayHi = hooks(
183 | async function (this: any, name: string) {
184 | return `${this.message} ${name}`;
185 | },
186 | middleware([
187 | async (ctx, next) => {
188 | ctx.arguments[0] += ' fn';
189 |
190 | await next();
191 | },
192 | ]),
193 | );
194 |
195 | const res = await o2.sayHi('Dave');
196 |
197 | assertStrictEquals(res, 'Hi Dave o1 o2 fn');
198 | });
199 |
200 | it('wraps an existing hooked function properly', async () => {
201 | const one = async (ctx: HookContext, next: NextFunction) => {
202 | await next();
203 |
204 | ctx.result += ' One';
205 | };
206 | const two = async (ctx: HookContext, next: NextFunction) => {
207 | await next();
208 |
209 | ctx.result += ' Two';
210 | };
211 | const three = async (ctx: HookContext, next: NextFunction) => {
212 | await next();
213 |
214 | ctx.result += ' Three';
215 | };
216 | const first = hooks(hello, middleware([one, two]));
217 | const second = hooks(first, middleware([three]));
218 | const mngr = getManager(second);
219 |
220 | if (mngr === null) {
221 | assert(false, 'There should be a manager');
222 | } else {
223 | assertEquals(mngr.getMiddleware(), [one, two, three]);
224 | }
225 |
226 | const result = await second('Dave');
227 |
228 | assertStrictEquals(result, 'Hello Dave Three Two One');
229 | });
230 |
231 | it('chains context and default initializers', async () => {
232 | const first = hooks(
233 | hello,
234 | middleware([], {
235 | params: ['name'],
236 | defaults() {
237 | return { defaulting: true };
238 | },
239 | }),
240 | );
241 | const second = hooks(
242 | first,
243 | middleware([
244 | async (ctx, next) => {
245 | assert(ctx.defaulting);
246 | ctx.name += ctx.testing;
247 | await next();
248 | },
249 | ]).props({ testing: ' test value' }),
250 | );
251 |
252 | const result = await second('Dave');
253 |
254 | assertStrictEquals(result, 'Hello Dave test value');
255 | });
256 |
257 | it('creates context with params and converts to arguments', async () => {
258 | const fn = hooks(
259 | hello,
260 | middleware([
261 | async (ctx, next) => {
262 | assertStrictEquals(ctx.name, 'Dave');
263 |
264 | ctx.name = 'Changed';
265 |
266 | await next();
267 | },
268 | ]).params('name'),
269 | );
270 |
271 | assertStrictEquals(await fn('Dave'), 'Hello Changed');
272 | });
273 |
274 | it('assigns props to context', async () => {
275 | const fn = hooks(
276 | hello,
277 | middleware([
278 | async (ctx, next) => {
279 | assertStrictEquals(ctx.name, 'Dave');
280 | assertStrictEquals(ctx.dev, true);
281 |
282 | ctx.name = 'Changed';
283 |
284 | await next();
285 | },
286 | ])
287 | .params('name')
288 | .props({ dev: true }),
289 | );
290 |
291 | assertStrictEquals(await fn('Dave'), 'Hello Changed');
292 | });
293 |
294 | it('assigns props to context by options', async () => {
295 | const fn = hooks(
296 | hello,
297 | middleware(
298 | [
299 | async (ctx, next) => {
300 | assertStrictEquals(ctx.name, 'Dave');
301 | assertStrictEquals(ctx.dev, true);
302 |
303 | ctx.name = 'Changed';
304 |
305 | await next();
306 | },
307 | ],
308 | {
309 | params: ['name'],
310 | props: { dev: true },
311 | },
312 | ),
313 | );
314 |
315 | assertStrictEquals(await fn('Dave'), 'Hello Changed');
316 | });
317 |
318 | it('ctx.arguments is configurable with named params', async () => {
319 | const modifyArgs = async (ctx: HookContext, next: NextFunction) => {
320 | ctx.arguments[0] = 'Changed';
321 | ctx.arguments.push('no');
322 |
323 | assertStrictEquals(ctx.name, ctx.arguments[0]);
324 |
325 | await next();
326 | };
327 |
328 | const fn = hooks(hello, middleware([modifyArgs]).params('name'));
329 |
330 | const customContext = fn.createContext();
331 | const resultContext = await fn('Daffl', {}, customContext);
332 |
333 | assertStrictEquals(resultContext, customContext);
334 | assertEquals(
335 | resultContext,
336 | fn.createContext({
337 | arguments: ['Changed', {}, 'no'],
338 | name: 'Changed',
339 | result: 'Hello Changed',
340 | }),
341 | );
342 | });
343 |
344 | it('can take and return an existing HookContext', async () => {
345 | const message = 'Custom message';
346 | const fn = hooks(
347 | hello,
348 | middleware([
349 | async (ctx, next) => {
350 | assertStrictEquals(ctx.name, 'Dave');
351 | assertStrictEquals(ctx.message, message);
352 |
353 | ctx.name = 'Changed';
354 | await next();
355 | },
356 | ]).params('name'),
357 | );
358 |
359 | const customContext = fn.createContext({ message });
360 | const resultContext: HookContext = await fn('Dave', {}, customContext);
361 |
362 | assertStrictEquals(resultContext, customContext);
363 | assertEquals(
364 | resultContext,
365 | fn.createContext({
366 | arguments: ['Changed', {}],
367 | message: 'Custom message',
368 | name: 'Changed',
369 | result: 'Hello Changed',
370 | }),
371 | );
372 | });
373 |
374 | it('calls middleware one time', async () => {
375 | let called = 0;
376 |
377 | const sayHi = hooks(
378 | (name: any) => `Hi ${name}`,
379 | middleware([
380 | async (_context, next) => {
381 | called++;
382 | await next();
383 | },
384 | ]),
385 | );
386 |
387 | const exclamation = hooks(
388 | sayHi,
389 | middleware([
390 | async (context, next) => {
391 | await next();
392 | context.result += '!';
393 | },
394 | ]),
395 | );
396 |
397 | const result = await exclamation('Bertho');
398 |
399 | assertStrictEquals(result, 'Hi Bertho!');
400 | assertStrictEquals(called, 1);
401 | });
402 |
403 | it('conserves method properties', async () => {
404 | const TEST = Symbol('test');
405 | const hello = (name: any) => `Hi ${name}`;
406 | (hello as any)[TEST] = true;
407 |
408 | const sayHi = hooks(
409 | hello,
410 | middleware([
411 | async (context, next) => {
412 | await next();
413 | context.result += '!';
414 | },
415 | ]),
416 | );
417 |
418 | const result = await sayHi('Bertho');
419 |
420 | assertStrictEquals(result, 'Hi Bertho!');
421 | assertStrictEquals((sayHi as any)[TEST], (hello as any)[TEST]);
422 | });
423 |
424 | it('works with array as middleware', async () => {
425 | const TEST = Symbol('test');
426 | const hello = (name: any) => `Hi ${name}`;
427 | (hello as any)[TEST] = true;
428 |
429 | const sayHi = hooks(hello, [
430 | async (context, next) => {
431 | await next();
432 | context.result += '!';
433 | },
434 | ]);
435 |
436 | const result = await sayHi('Bertho');
437 |
438 | assertStrictEquals(result, 'Hi Bertho!');
439 | assertStrictEquals((sayHi as any)[TEST], (hello as any)[TEST]);
440 | });
441 |
442 | it('context has own properties', async () => {
443 | const fn = hooks(hello, middleware([]).params('name'));
444 |
445 | const customContext = fn.createContext({ message: 'Hi !' });
446 | const resultContext: HookContext = await fn('Dave', {}, customContext);
447 | const keys = Object.keys(resultContext);
448 |
449 | assert(keys.includes('self'));
450 | assert(keys.includes('message'));
451 | assert(keys.includes('arguments'));
452 | assert(keys.includes('result'));
453 | });
454 |
455 | it('same params and props throw an error', async () => {
456 | const hello = async (name?: string) => {
457 | return `Hello ${name}`;
458 | };
459 | assertThrows(
460 | () => hooks(hello, middleware([]).params('name').props({ name: 'David' })),
461 | undefined,
462 | `Hooks can not have a property and param named 'name'. Use .defaults instead.`,
463 | );
464 | });
465 |
466 | it('creates context with default params', async () => {
467 | const fn = hooks(
468 | hello,
469 | middleware([
470 | async (ctx, next) => {
471 | assertEquals(ctx.params, {});
472 |
473 | await next();
474 | },
475 | ])
476 | .params('name', 'params')
477 | .defaults(() => {
478 | return {
479 | name: 'Bertho',
480 | params: {},
481 | };
482 | }),
483 | );
484 |
485 | assertStrictEquals(await fn('Dave'), 'Hello Dave');
486 | assertStrictEquals(await fn(), 'Hello Bertho');
487 | });
488 |
--------------------------------------------------------------------------------
/main/hooks/test/object.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertStrictEquals, assertThrows, it } from './dependencies.ts';
2 | import { HookContext, hooks, middleware, NextFunction } from '../src/index.ts';
3 |
4 | interface HookableObject {
5 | test: string;
6 | sayHi(name: string): Promise;
7 | addOne(number: number): Promise;
8 | }
9 |
10 | const getObject = (): HookableObject => ({
11 | test: 'me',
12 |
13 | async sayHi(name: string) {
14 | return `Hi ${name}`;
15 | },
16 |
17 | async addOne(number: number) {
18 | return number + 1;
19 | },
20 | });
21 |
22 | it('hooks object with hook methods, sets method name', async () => {
23 | const obj = getObject();
24 |
25 | const hookedObj = hooks(obj, {
26 | sayHi: middleware([
27 | async (ctx: HookContext, next: NextFunction) => {
28 | assertEquals(ctx.arguments, ['David']);
29 | assertEquals(ctx.method, 'sayHi');
30 | assertEquals(ctx.self, obj);
31 |
32 | await next();
33 |
34 | ctx.result += '?';
35 | },
36 | ]),
37 | addOne: middleware([
38 | async (ctx: HookContext, next: NextFunction) => {
39 | ctx.arguments[0] += 1;
40 |
41 | await next();
42 | },
43 | ]),
44 | });
45 |
46 | assertStrictEquals(obj, hookedObj);
47 | assertEquals(await hookedObj.sayHi('David'), 'Hi David?');
48 | assertEquals(await hookedObj.addOne(1), 3);
49 | });
50 |
51 | it('hooks object and allows to customize context for method', async () => {
52 | const obj = getObject();
53 | const hookedObj = hooks(obj, {
54 | sayHi: middleware([
55 | async (ctx: HookContext, next: NextFunction) => {
56 | assertEquals(ctx.arguments, ['David']);
57 | assertEquals(ctx.method, 'sayHi');
58 | assertEquals(ctx.name, 'David');
59 | assertEquals(ctx.self, obj);
60 |
61 | ctx.name = 'Dave';
62 |
63 | await next();
64 |
65 | ctx.result += '?';
66 | },
67 | ]).params('name'),
68 |
69 | addOne: middleware([
70 | async (ctx: HookContext, next: NextFunction) => {
71 | ctx.arguments[0] += 1;
72 |
73 | await next();
74 | },
75 | ]),
76 | });
77 |
78 | assertStrictEquals(obj, hookedObj);
79 | assertEquals(await hookedObj.sayHi('David'), 'Hi Dave?');
80 | assertEquals(await hookedObj.addOne(1), 3);
81 | });
82 |
83 | it('hooking multiple times works properly', async () => {
84 | const obj = getObject();
85 |
86 | hooks(obj, {
87 | sayHi: middleware([
88 | async (ctx: HookContext, next: NextFunction) => {
89 | await next();
90 |
91 | ctx.result += '?';
92 | },
93 | ]),
94 | });
95 |
96 | hooks(obj, {
97 | sayHi: middleware([
98 | async (ctx: HookContext, next: NextFunction) => {
99 | await next();
100 |
101 | ctx.result += '!';
102 | },
103 | ]),
104 | });
105 |
106 | assertEquals(await obj.sayHi('David'), 'Hi David!?');
107 | });
108 |
109 | it('throws an error when hooking invalid method', async () => {
110 | const obj = getObject();
111 |
112 | assertThrows(
113 | () =>
114 | hooks(obj, {
115 | test: middleware([
116 | async (_ctx, next) => {
117 | await next();
118 | },
119 | ]),
120 | }),
121 | undefined,
122 | `Can not apply hooks. 'test' is not a function`,
123 | );
124 | });
125 |
126 | it('works with object level hooks', async () => {
127 | const obj = getObject();
128 |
129 | hooks(obj, [
130 | async (ctx: HookContext, next: NextFunction) => {
131 | await next();
132 |
133 | ctx.result += '!';
134 | },
135 | ]);
136 |
137 | hooks(obj, {
138 | sayHi: middleware([
139 | async (ctx: HookContext, next: NextFunction) => {
140 | await next();
141 |
142 | ctx.result += '?';
143 | },
144 | ]),
145 | });
146 |
147 | assertEquals(await obj.sayHi('Dave'), 'Hi Dave?!');
148 | });
149 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | format:
2 | deno fmt main --config="deno.json"
3 |
4 | lint:
5 | deno lint main --config="deno.json"
6 |
7 | test: format lint
8 | deno test main/
9 |
10 | coverage: format lint
11 | deno test main/ --coverage=coverage
12 | deno coverage coverage
13 |
14 | npm:
15 | rm -rf packages/
16 | deno run -A main/build.ts
17 | npx lerna bootstrap
18 |
19 | publish: npm
20 | npx lerna publish
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@feathersjs/hooks",
3 | "private": true,
4 | "workspaces": [
5 | "packages/*"
6 | ],
7 | "devDependencies": {
8 | "lerna": "^6.0.0"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | @feathersjs/hooks
2 |
3 | [](https://github.com/feathersjs/hooks/actions/workflows/deno.yml)
4 |
5 | `@feathersjs/hooks` brings middleware-like functionality to any async JavaScript or TypeScript function. It allows creation of composable and reusable workflows to handle functionality like
6 |
7 | - Logging
8 | - Profiling
9 | - Validation
10 | - Caching / Debouncing
11 | - Permissions
12 | - Data pre- and post-processing
13 | - etc.
14 |
15 | This functionality can be added without having to change the original function. The pattern also keeps everything cleanly separated and testable.
16 |
17 | ```ts
18 | import { hooks } from '@feathersjs/hooks';
19 |
20 | // We're going to wrap `sayHi` with hook middleware.
21 | class Hello {
22 | async sayHi(name) {
23 | return `Hi ${name}`;
24 | }
25 | }
26 |
27 | // This logRuntime hook will be used as middleware
28 | const logRuntime = async (context, next) => {
29 | const start = new Date().getTime();
30 |
31 | await next(); // In this example, `next` is `sayHi`.
32 |
33 | const duration = new Date().getTime() - start;
34 | console.log(
35 | `Function '${context.method}' returned '${context.result}' after ${duration}ms`
36 | );
37 | };
38 |
39 | // The `hooks` utility wraps `logRuntime` around `sayHi`.
40 | hooks(Hello, {
41 | sayHi: [logRuntime]
42 | });
43 |
44 | // Calling `sayHi` will start by calling the `logRuntime` hook.
45 | (async () => {
46 | const hi = new Hello();
47 |
48 | console.log(await hi.sayHi('Dave'));
49 | })();
50 | ```
51 |
52 | The Hooks middleware pattern was originally implemented directly in [FeathersJS](https://www.feathersjs.com). Having been recognized as a powerful pattern with more broad-scale usefulness, it has been extracted from FeathersJS into this standalone utility.
53 |
54 | See the [⚓ release post for a quick overview](https://blog.feathersjs.com/async-middleware-for-javascript-and-typescript-31a0f74c0d30).
55 |
56 | - [Installation](#installation)
57 | - [Node](#node)
58 | - [Deno](#deno)
59 | - [Browser](#browser)
60 | - [Documentation](#documentation)
61 | - [Intro to Async Hooks](#intro-to-async-hooks)
62 | - [The `hooks` Function](#the-hooks-function)
63 | - [Example with a Function](#example-with-a-function)
64 | - [Example with a Class](#example-with-a-class)
65 | - [TypeScript with the `@hooks` Decorator](#typescript-with-the-hooks-decorator)
66 | - [The `middleware` Manager](#the-middleware-manager)
67 | - [params(...names)](#paramsnames)
68 | - [props(properties)](#propsproperties)
69 | - [defaults(callback)](#defaultscallback)
70 | - [Global Hooks](#global-hooks)
71 | - [Global Hooks on an Object](#global-hooks-on-an-object)
72 | - [Global Hooks on a Class](#global-hooks-on-a-class)
73 | - [JavaScript Example](#javascript-example)
74 | - [TypeScript Example](#typescript-example)
75 | - [Hook Context](#hook-context)
76 | - [Context properties](#context-properties)
77 | - [Arguments](#arguments)
78 | - [Using named parameters](#using-named-parameters)
79 | - [Default values](#default-values)
80 | - [Modifying the result](#modifying-the-result)
81 | - [Calling the original function](#calling-the-original-function)
82 | - [Customizing and returning the context](#customizing-and-returning-the-context)
83 | - [Flow Control with Multiple Hooks](#flow-control-with-multiple-hooks)
84 | - [Async Hook Flow](#async-hook-flow)
85 | - [Regular Hooks](#regular-hooks)
86 | - [The `collect` utility](#the-collect-utility)
87 | - [Best practises](#best-practises)
88 | - [More Examples](#more-examples)
89 | - [Cache](#cache)
90 | - [Permissions](#permissions)
91 | - [Cleaning up GraphQL resolvers](#cleaning-up-graphql-resolvers)
92 | - [Contributing](#contributing)
93 | - [License](#license)
94 |
95 | # Installation
96 |
97 | ## Node
98 |
99 | ```shell
100 | npm install @feathersjs/hooks --save
101 | yarn add @feathersjs/hooks
102 | ```
103 |
104 | ## Deno
105 |
106 | `@feathersjs/hooks` releases are published to [deno.land/x/hooks](https://deno.land/x/hooks):
107 |
108 | ```ts
109 | import { hooks } from 'https://deno.land/x/hooks/src/index.ts';
110 | ```
111 |
112 | ## Browser
113 |
114 | The `@feathersjs/hooks` npm package works in any modern browser and is compatible with any module loader like Webpack.
115 |
116 | # Documentation
117 |
118 | ## Intro to Async Hooks
119 |
120 | The fundamental building block of `@feathersjs/hooks` is the "Async Hook". An "Async Hook" is an `async` function that accepts two arguments:
121 |
122 | - A [`context` object](#hook-context) containing the arguments for the function call.
123 | - An asynchronous `next` function. Somewhere in the body of a hook function, there is a call to `await next()`, which calls the `next` hook OR the original function if all other hooks have run.
124 |
125 | In its simplest form, an Async Hook looks like this:
126 |
127 | ```js
128 | const myAsyncHook = async (context, next) => {
129 | // Code before `await next()` runs before the main function
130 | await next();
131 | // Code after `await next()` runs after the main function.
132 | };
133 | ```
134 |
135 | Any Async Hook can be wrapped around another function, essentially becoming a middleware function. Calling `await next()` will either call the next middleware in the chain or the original function if all middleware have run. In the next section you'll learn how to wrap hooks around other functions.
136 |
137 | ## The `hooks` Function
138 |
139 | `hooks(fn, middleware[]|manager)` returns a new function that wraps `fn` with `middleware`
140 |
141 | The `hooks` function wraps one or more [Async Hooks](#intro-to-async-hooks) around another function, setting up the hooks as middleware. The following examples all show the default functionality of passing an array of hooks as the second argument. Learn about additional functionality in the section about [Middleware Managers](#the-middleware-manager)
142 |
143 | ### Example with a Function
144 |
145 | The example below demonstrates the concept of wrapping the `make_request` function with the `verify_auth` hook function.
146 |
147 | ```ts
148 | import { hooks } from '@feathersjs/hooks';
149 |
150 | const make_request = () => {
151 | /* make a request to the database server */
152 | };
153 |
154 | const verify_auth = (context, next) => {
155 | /* Do auth verification before calling `await next()` */
156 | await next();
157 | };
158 |
159 | const request_with_middleware = hooks(make_request, [verify_auth]);
160 | ```
161 |
162 | In the above example, calling `request_with_middleware` will call the `verify_auth` function before calling `make_request`. The `verify_auth` function will have a `context.arguments` array containing the original arguments for the function call. A hook can modify the context object before calling `await next()`. (In this case, the `next` function IS the `make_request` function.) Alternatively, `verify_auth` could throw an error to prevent the request from ever getting to the `make_request` function. Check the [hook context](#hook-context) section to learn how to turn the `context.arguments` array into named parameters.
163 |
164 | > **Important:** A wrapped function will _always_ return a Promise even if it was not originally `async`.
165 |
166 | We've seen how to wrap a single function, but the `hooks` utility is more powerful. It can also wrap [object methods](#object-hooks) and [class methods](#class-hooks). The following example shows how to use it with a class.
167 |
168 | ### Example with a Class
169 |
170 | The following example updates a class's `sayHi` method to log information about a function call. This syntax also works on plain objects.
171 |
172 | ```js
173 | const { hooks } = require('@feathersjs/hooks');
174 |
175 | // This class has a `sayHi` instance method we're going to wrap with hooks.
176 | // This would also work with an object containing a `sayHi` method.
177 | class Hello {
178 | async sayHi(name) {
179 | return `Hi ${name}`;
180 | }
181 | }
182 |
183 | // This logRuntime hook will be used as middleware
184 | const logRuntime = async (context, next) => {
185 | // Code before `await next()` runs before the original function
186 | const start = new Date().getTime();
187 |
188 | await next();
189 |
190 | // Code after `await next()` runs after the original function.
191 | const end = new Date().getTime();
192 | console.log(
193 | `Function '${context.method || '[no name]'}' returned '${
194 | context.result
195 | }' after ${end - start}ms`
196 | );
197 | };
198 |
199 | // Enhance class (or object) methods using an object of method names as the 2nd argument
200 | hooks(Hello, { sayHi: [logRuntime] });
201 |
202 | // You can now use the wrapped instance methods inside any async function.
203 | (async () => {
204 | const hi = new Hello();
205 |
206 | console.log(await hi.sayHi('Dave'));
207 | })();
208 | ```
209 |
210 | ### TypeScript with the `@hooks` Decorator
211 |
212 | With TypeScript, you can use `hooks` the same was as shown in the above JavaScript example, or - as of TypeScript 5 - use the `@hooks` decorator:
213 |
214 | ```ts
215 | import { hooks, HookContext, NextFunction } from '@feathersjs/hooks';
216 |
217 | const logRuntime = async (context: HookContext, next: NextFunction) => {
218 | const start = new Date().getTime();
219 |
220 | await next();
221 |
222 | const end = new Date().getTime();
223 | console.log(
224 | `Function '${context.method || '[no name]'}' returned '${
225 | context.result
226 | }' after ${end - start}ms`
227 | );
228 | };
229 |
230 | class Hello {
231 | @hooks([logRuntime]) // the @hooks decorator
232 | async sayHi(name: string) {
233 | return `Hi ${name}`;
234 | }
235 | }
236 |
237 | (async () => {
238 | const hi = new Hello();
239 |
240 | console.log(await hi.sayHi('David'));
241 | })();
242 | ```
243 |
244 | > **Note:** An `experimentalDecorators` compatible decorator for earlier TypeScript versions can be used via `import { legacyDecorator } from '@feathersjs/hooks'`.
245 |
246 | ## The `middleware` Manager
247 |
248 | You can use a `middleware` manager, instead of a plain array of hook functions, to enable additional functionality.
249 |
250 | In all previous examples, when calling `hooks` with an array in the second argument ﹣ either directly like `hooks(someFn, [])` or as the value of an object key like `hooks(someObj, { prop: [] })` ﹣ the array gets wrapped into an internal middleware `Manager`.
251 |
252 | The `middleware` function creates a middleware Manager which has three important methods:
253 |
254 | - `params()`
255 | - `props()`
256 | - `defaults()`
257 |
258 | ```js
259 | const { hooks, middleware } = require('@feathersjs/hooks');
260 |
261 | const sayHiWithHooks = hooks(sayHi, middleware([hook1, hook2, hook3]));
262 |
263 | (async () => {
264 | await sayHiWithHooks('David');
265 | })();
266 | ```
267 |
268 | ### params(...names)
269 |
270 | Supplies names for original function arguments. Instead of appearing in `params.arguments`, the arguments will be named in the order provided.
271 |
272 | ```js
273 | const sayHiWithHooks = hooks(
274 | sayHi,
275 | middleware([hook1, hook2, hook3]).params('name', 'age')
276 | );
277 | ```
278 |
279 | ### props(properties)
280 |
281 | Initializes properties on the `context`
282 |
283 | ```js
284 | const sayHiWithHooks = hooks(
285 | sayHi,
286 | middleware([hook1, hook2, hook3])
287 | .params('name')
288 | .props({ customProperty: true })
289 | );
290 | ```
291 |
292 | > **Note:** `.props` must not contain any of the field names defined in `.params`.
293 |
294 | ### defaults(callback)
295 |
296 | Calls a `callback(self, arguments, context)` that returns default values which will be set if the property on the hook context is `undefined`. Applies to both, `params` and other properties.
297 |
298 | ```js
299 | const sayHi = async (name) => `Hello ${name}`;
300 |
301 | const sayHiWithHooks = hooks(
302 | sayHi,
303 | middleware([])
304 | .params('name')
305 | .defaults((self, args, context) => {
306 | return {
307 | name: 'Unknown human'
308 | };
309 | })
310 | );
311 | ```
312 |
313 | ## Global Hooks
314 |
315 | Sometimes you want to run a set of hooks on all of the methods in a class or object.
316 |
317 | ### Global Hooks on an Object
318 |
319 | Hooks can also be registered at the object level which will run before any specific hooks on a hook enabled function:
320 |
321 | ```js
322 | const { hooks } = require('@feathersjs/hooks');
323 |
324 | const o = {
325 | async sayHi (name) {
326 | return `Hi ${name}!`;
327 | }
328 | async sayHello (name) {
329 | return `Hello ${name}!`;
330 | }
331 | }
332 |
333 | // This hook will run first for every hook-enabled method on the object
334 | hooks(o, [
335 | async (context, next) => {
336 | console.log('Top level hook');
337 | await next();
338 | }
339 | ]);
340 | // The global hooks only run if you enable hooks on the method:
341 | hooks(o, {
342 | sayHello: middleware([ logRuntime ]).params('name', 'quote'),
343 | sayHi: []
344 | });
345 |
346 | hooks(o, {
347 | sayHi: [ logRuntime ]
348 | });
349 | ```
350 |
351 | ### Global Hooks on a Class
352 |
353 | Similar to object hooks, class hooks modify the class (or class prototype). Just like for objects it is possible to register hooks that are global to the class or object. Registering hooks also works with inheritance.
354 |
355 | > **Note:** Object or class level global hooks will only run if the method itself has been enabled for hooks. This can be done by registering hooks with an empty array.
356 |
357 | #### JavaScript Example
358 |
359 | ```js
360 | const { hooks } = require('@feathersjs/hooks');
361 |
362 | class HelloSayer {
363 | async sayHello(name) {
364 | return `Hello ${name}`;
365 | }
366 | }
367 |
368 | class HappyHelloSayer extends HelloSayer {
369 | async sayHello(name) {
370 | const baseHello = await super.sayHello(name);
371 | return baseHello + '!!!!! :)';
372 | }
373 | }
374 |
375 | // Add global hooks to the class using its prototype
376 | hooks(HelloSayer.prototype, [
377 | async (context, next) => {
378 | console.log('Hook on HelloSayer');
379 | await next();
380 | }
381 | ]);
382 |
383 | hooks(HappyHelloSayer.prototype, [
384 | async (context, next) => {
385 | console.log('Hook on HappyHelloSayer');
386 | await next();
387 | }
388 | ]);
389 |
390 | // Enabling hooks on sayHello also allows the global hooks to run.
391 | hooks(HelloSayer, {
392 | sayHello: [
393 | async (context, next) => {
394 | console.log('Hook on HelloSayer.sayHello');
395 | await next();
396 | }
397 | ]
398 | });
399 |
400 | (async () => {
401 | const happy = new HappyHelloSayer();
402 |
403 | console.log(await happy.sayHello('David'));
404 | })();
405 | ```
406 |
407 | #### TypeScript Example
408 |
409 | Using decorators in TypeScript also respects inheritance:
410 |
411 | ```js
412 | import { hooks, HookContext, NextFunction } from '@feathersjs/hooks';
413 |
414 | @hooks([
415 | async (context: HookContext, next: NextFunction) => {
416 | console.log('Hook on HelloSayer');
417 | await next();
418 | }
419 | ])
420 | class HelloSayer {
421 | @hooks(
422 | middleware([
423 | async (context: HookContext, next: NextFunction) => {
424 | console.log('Hook on HelloSayer.sayHello');
425 | await next();
426 | }
427 | ]).params('name')
428 | )
429 | async sayHello(name: string) {
430 | return `Hello ${name}`;
431 | }
432 |
433 | async otherMethod() {
434 | return 'This will not run any hooks';
435 | }
436 | }
437 |
438 | @hooks([
439 | async (context: HookContext, next: NextFunction) => {
440 | console.log('Hook on HappyHelloSayer');
441 | await next();
442 | }
443 | ])
444 | class HappyHelloSayer extends HelloSayer {
445 | async sayHello(name: string) {
446 | const message = await super.sayHello(name);
447 | return `${message}!!!!! :)`;
448 | }
449 | }
450 |
451 | (async () => {
452 | const happy = new HappyHelloSayer();
453 |
454 | console.log(await happy.sayHello('David'));
455 | })();
456 | ```
457 |
458 | > **Note:** Decorators only work on classes and class methods, not on functions. Standalone (arrow) functions require the [JavaScript function style](#function-hooks) hook registration.
459 |
460 | ## Hook Context
461 |
462 | The hook `context` in a [middleware function](#middleware) is an object that contains information about the function call.
463 |
464 | ### Context properties
465 |
466 | The default properties available are:
467 |
468 | - `context.arguments` - The arguments of the function as an array
469 | - `context.method` - The name of the function (if it belongs to an object or class)
470 | - `context.self` - The `this` context of the function being called (may not always be available e.g. for top level arrow functions)
471 | - `context.result` - The result of the method call
472 | - `context[name]` - Value of a named parameter when [using named arguments](#using-named-parameters)
473 |
474 | ### Arguments
475 |
476 | By default, the function call arguments will be available as an array in `context.arguments`. The values can be modified to change what is passed to the original function call:
477 |
478 | ```js
479 | const { hooks } = require('@feathersjs/hooks');
480 |
481 | const sayHello = async (firstName, lastName) => {
482 | return `Hello ${firstName} ${lastName}!`;
483 | };
484 |
485 | const wrappedSayHello = hooks(sayHello, [
486 | async (context, next) => {
487 | // Replace the `lastName`
488 | context.arguments[1] = 'X';
489 | await next();
490 | }
491 | ]);
492 |
493 | (async () => {
494 | console.log(await wrappedSayHello('David', 'L')); // Hello David X
495 | })();
496 | ```
497 |
498 | ### Using named parameters
499 |
500 | It is also possible to turn the arguments into named parameters. In the above example we probably want to have `context.firstName` and `context.lastName` available. To do this, the [`context` option](#options) can be initialized like this:
501 |
502 | ```js
503 | const { hooks, middleware } = require('@feathersjs/hooks');
504 |
505 | const sayHello = async (firstName, lastName) => {
506 | return `Hello ${firstName} ${lastName}!`;
507 | };
508 |
509 | const wrappedSayHello = hooks(
510 | sayHello,
511 | middleware([
512 | async (context, next) => {
513 | // Now we can modify `context.lastName` instead
514 | context.lastName = 'X';
515 | await next();
516 | }
517 | ]).params('firstName', 'lastName')
518 | );
519 |
520 | (async () => {
521 | console.log(await wrappedSayHello('David', 'L')); // Hello David X
522 | })();
523 | ```
524 |
525 | > **Note:** When using named parameters, `context.arguments` is read only to preserve the order of named params.
526 |
527 | ### Default values
528 |
529 | You can add default values using the manager's `.defaults()` method. See [manager.defaults()](#defaultscallback)
530 |
531 | > **Note:** Even if your original function contains a default value, it is important to specify it because the middleware runs before and the value will be `undefined` without a default value.
532 |
533 | ### Modifying the result
534 |
535 | In a hook function, `context.result` can be
536 |
537 | - Set _before_ calling `await next()` to skip the original function call. Other hooks will still run.
538 | - Modified _after_ calling `await next()` to modify what is being returned by the function.
539 |
540 | See the [cache example](#cache) for how this can be used.
541 |
542 | ### Calling the original function
543 |
544 | The original function without any hooks is available as `fn.original`:
545 |
546 | ```js
547 | const { hooks } = require('@feathersjs/hooks');
548 | const emphasize = async (context, next) => {
549 | await next();
550 |
551 | context.result += '!!!';
552 | };
553 | const sayHello = hooks(async (name) => `Hello ${name}`, [emphasize]);
554 |
555 | const o = hooks(
556 | {
557 | async sayHi(name) {
558 | return `Hi ${name}`;
559 | }
560 | },
561 | {
562 | sayHi: [emphasize]
563 | }
564 | );
565 |
566 | (async () => {
567 | console.log(await sayHello.original('Dave')); // Hello Dave
568 | // Originals on object need to be called with an explicit `this` context
569 | console.log(await o.sayHi.original.call(o, 'David'));
570 | })();
571 | ```
572 |
573 | ### Customizing and returning the context
574 |
575 | Once a function has been wrapped with `hooks`, the wrapped function will have a `createContext` method. This method can be used to create a custom context object. This custom context can then be passed as the last argument of a hook-enabled function call. In that case, the up-to-date context object - with all the information (like `context.result`) - will be returned:
576 |
577 | ```js
578 | const { hooks, HookContext } = require('@feathersjs/hooks');
579 | const customContextData = async (context, next) => {
580 | console.log('Custom context message is', context.message);
581 |
582 | context.customProperty = 'Hi';
583 |
584 | await next();
585 | };
586 |
587 | const sayHello = hooks(
588 | async (message) => {
589 | return `Hello ${message}!`;
590 | },
591 | [customContextData]
592 | );
593 |
594 | const customContext = sayHello.createContext({
595 | message: 'Hi from context'
596 | });
597 |
598 | (async () => {
599 | const finalContext = await sayHello('Dave', customContext);
600 |
601 | console.log(finalContext);
602 | })();
603 | ```
604 |
605 | ## Flow Control with Multiple Hooks
606 |
607 | ### Async Hook Flow
608 |
609 | Middleware functions (or hook functions) take a `context` and an asynchronous `next` function as their parameters. The `context` contains information about the function call (like the arguments, the result or `this` context) and the `next` function can be called to continue to the next hook or the original function.
610 |
611 | A middleware function can do things before calling `await next()` and after all following middleware functions and the function call itself return. It can also `try/catch` the `await next()` call to handle and modify errors. This is the same control flow that the web framework [KoaJS](https://koajs.com/) uses for handling HTTP requests and response.
612 |
613 | Each hook function wraps _around_ all other functions (like an onion). This means that the first registered middleware function will run first before `await next()` and as the very last after all following hooks.
614 |
615 | 
616 |
617 | The following example uses hooks named `one`, `two`, and `three` to demonstrate how execution order works:
618 |
619 | ```js
620 | const { hooks } = require('@feathersjs/hooks');
621 |
622 | const sayHello = async (message) => {
623 | console.log(`HELLO, ${message}!`);
624 | };
625 |
626 | const one = async (ctx, next) => {
627 | console.log('one before');
628 | await next();
629 | console.log('one after');
630 | };
631 |
632 | const two = async (ctx, next) => {
633 | console.log('two before');
634 | await next();
635 | console.log('two after');
636 | };
637 |
638 | const three = async (ctx, next) => {
639 | console.log('three before');
640 | await next();
641 | console.log('three after');
642 | };
643 |
644 | const sayHelloWithHooks = hooks(sayHello, [one, two, three]);
645 |
646 | (async () => {
647 | await sayHelloWithHooks('DAVID');
648 | })();
649 | ```
650 |
651 | Would print:
652 |
653 | ```console
654 | one before
655 | two before
656 | three before
657 | HELLO, DAVID!
658 | three after
659 | two after
660 | one after
661 | ```
662 |
663 | This order also applies when using hooks on [objects](#object-hooks) and [classes and with inheritance](#class-hooks).
664 |
665 | ### Regular Hooks
666 |
667 | You may have noticed that after-hook execution order is the reverse compared to before-hook execution order. This is due to how the hooks wrap around each other. If you prefer that the flow of the hooks matches the flow of the page, you can use Regular Hooks. Regular Hooks are similar to Async Hooks, but they do not receive a `next` function as the second argument. This means there is no `async next()` in the middle of the function body. This allows the code execution to match the natural reading flow on the page: top to bottom. Here's what a regular hook looks like:
668 |
669 | ```js
670 | // A Regular Hook is just an async function that receives the context object.
671 | const regularHook = async (context) => {
672 | // All code goes here.
673 | };
674 | ```
675 |
676 | With @feathersjs/hooks, the `collect` utility enables the use of Regular Hooks.
677 |
678 | > Longtime FeathersJS developers will recognize Regular Hooks. They're the same type of hooks that have been around since the beginning.
679 |
680 | #### The `collect` utility
681 |
682 | The `collect` utility enables Regular Hooks functionality. It gathers hooks into `before`, `after`, and `error` hooks. Here's what it looks like.
683 |
684 | ```ts
685 | import { hooks } from '@feathersjs/hooks';
686 | import { discard } from 'feathers-hooks-common';
687 |
688 | const make_request = () => {
689 | /* make a request to the database server */
690 | };
691 |
692 | const verify_auth = (context) => {
693 | /* Do auth verification, here */
694 | };
695 | const handle_error = (context) => {
696 | /* Do some error handling */
697 | };
698 |
699 | const request_with_middleware = hooks(
700 | make_request,
701 | middleware([
702 | collect({
703 | before: [verify_auth],
704 | after: [discard('password')],
705 | error: [handle_error]
706 | })
707 | ])
708 | );
709 | ```
710 |
711 | Or with a class:
712 |
713 | ```ts
714 | import { hooks } from '@feathersjs/hooks';
715 | import { discard } from 'feathers-hooks-common';
716 |
717 | class DbAdapter {
718 | create() {
719 | /* create data in the db */
720 | }
721 | }
722 |
723 | const verify_auth = (context) => {
724 | /* Do auth verification, here */
725 | };
726 | const handle_error = (context) => {
727 | /* Do some error handling */
728 | };
729 |
730 | const request_with_middleware = hooks(DbAdapter, {
731 | create: middleware([
732 | collect({
733 | before: [verify_auth],
734 | after: [discard('password')],
735 | error: [handle_error]
736 | })
737 | ])
738 | });
739 | ```
740 |
741 | # Best practises
742 |
743 | - Hooks can be registered at any time by calling `hooks` again but registration should be kept in one place for better visibility.
744 | - Decorators make the flow even more visible by putting it right next to the code the hooks are affecting.
745 | - The `context` will always be the same object in the hook flow. You can set any property on it.
746 | - If a parameter is an object, modifying that object will change the original parameter. This can cause subtle issues that are difficult to debug. Using the spread operator to add the new property and replacing the context property helps to avoid many of those problems:
747 |
748 | ```js
749 | const updateQuery = async (context, next) => {
750 | // NOT: context.query.newProperty = 'something';
751 |
752 | // Instead
753 | context.query = {
754 | ...context.query,
755 | active: true
756 | };
757 |
758 | await next();
759 | };
760 |
761 | const findUser = hooks(async (query) => {
762 | return collection.find(query);
763 | }, middleware([updateQuery]).params('query'));
764 | ```
765 |
766 | # More Examples
767 |
768 | ## Cache
769 |
770 | The following example is a simple hook that caches the results of a function call and uses the cached value. It will clear the cache every 5 seconds. This is useful for any kind of expensive method call like an external HTTP request:
771 |
772 | ```js
773 | const { hooks } = require('@feathersjs/hooks');
774 | const cache = () => {
775 | let cacheData = {};
776 |
777 | // Reset entire cache every 5 seconds
778 | setInterval(() => {
779 | cacheData = {};
780 | }, 5000);
781 |
782 | return async (context, next) => {
783 | const key = JSON.stringify(context);
784 |
785 | if (cacheData[key]) {
786 | // Setting context.result before `await next()`
787 | // will skip the (expensive function call) and
788 | // make it return the cached value
789 | context.result = cacheData[key];
790 | }
791 |
792 | await next();
793 |
794 | // Set the cached value to the result
795 | cacheData[key] = context.result;
796 | };
797 | };
798 |
799 | const getData = hooks(
800 | async (url) => {
801 | return axios.get(url);
802 | },
803 | [cache()]
804 | );
805 |
806 | await getData('http://url-that-takes-long-to-respond');
807 | ```
808 |
809 | ## Permissions
810 |
811 | When passing e.g. a `user` object to a function call, hooks allow for a better separation of concerns by handling permissions in a hook:
812 |
813 | ```js
814 | const checkPermission = (name) => async (context, next) => {
815 | if (!context.user.permissions.includes(name)) {
816 | throw new Error(`User does not have ${name} permission`);
817 | }
818 |
819 | await next();
820 | };
821 |
822 | const deleteInvoice = hooks(async (id, user) => {
823 | return collection.delete(id);
824 | }, middleware([checkPermission('admin')]).params('id', 'user'));
825 | ```
826 |
827 | ## Cleaning up GraphQL resolvers
828 |
829 | The above examples can both be useful for speeding up and locking down existing [GraphQL resolvers](https://graphql.org/learn/execution/):
830 |
831 | ```js
832 | const { hooks } = require('@feathersjs/hooks');
833 |
834 | const checkPermission = (name) => async (ctx, next) => {
835 | const { context } = ctx;
836 | if (!context.user.permissions.includes(name)) {
837 | throw new Error(`User does not have ${name} permission`);
838 | }
839 |
840 | await next();
841 | };
842 |
843 | const resolvers = {
844 | Query: {
845 | human: hooks(async (obj, args, context, info) => {
846 | return context.db
847 | .loadHumanByID(args.id)
848 | .then((userData) => new Human(userData));
849 | }, middleware([cache(), checkPermission('admin')]).params('obj', 'args', 'context', 'info'))
850 | }
851 | };
852 | ```
853 |
854 | # Contributing
855 |
856 | For general contribution information refer to the [Feathers contribution guideLINES](https://github.com/feathersjs/hooks/blob/master/.github/contributing.md).
857 |
858 | `@feathersjs/hooks` modules are written in TypeScript using [Deno](https://deno.land/) as the runtime. With Deno 1.16 or later installed you can start contributing by cloning this repository or your own fork via:
859 |
860 | ```
861 | git clone git@github.com:feathersjs/hooks.git
862 | cd hooks/
863 | ```
864 |
865 | And then run the tests via:
866 |
867 | ```
868 | make test
869 | ```
870 |
871 | # License
872 |
873 | Copyright (c) 2021
874 |
875 | Licensed under the [MIT license](LICENSE).
876 |
--------------------------------------------------------------------------------