Compare commits

..

86 Commits

Author SHA1 Message Date
Kamil Myśliwiec
691e7d448e chore: minor formatting changes 2024-11-20 12:52:50 +01:00
Kamil Myśliwiec
518879b3d6 Merge branch 'test-sample-mongoose' of https://github.com/martinvysnovsky/nest into martinvysnovsky-test-sample-mongoose 2024-11-20 12:50:39 +01:00
Kamil Myśliwiec
b770d7d9b2 Merge branch 'master' of https://github.com/nestjs/nest 2024-11-20 12:49:16 +01:00
Kamil Myśliwiec
8b978e3028 Merge branch 'Yansb-test/sample-01-cats-app' 2024-11-20 12:49:03 +01:00
Kamil Myśliwiec
0f25e83229 style: minor formatting changes 2024-11-20 12:48:53 +01:00
Kamil Myśliwiec
f3ec5f22fa Merge branch 'test/sample-01-cats-app' of https://github.com/Yansb/nest into Yansb-test/sample-01-cats-app 2024-11-20 12:48:06 +01:00
Kamil Mysliwiec
1dafb5fdce Merge pull request #14165 from nestjs/dependabot/npm_and_yarn/typescript-eslint/parser-8.15.0
chore(deps-dev): bump @typescript-eslint/parser from 8.14.0 to 8.15.0
2024-11-20 12:46:02 +01:00
Kamil Mysliwiec
71e8acec39 Merge pull request #14163 from nestjs/fix/grpc-client-streaming-bug
fix(microservices): grpc client streaming bugs
2024-11-20 11:11:51 +01:00
Kamil Mysliwiec
6094701679 Merge pull request #12954 from gunb0s/feature/nestjs-kafka-emit-batch
feat: emit batch
2024-11-20 11:11:43 +01:00
Kamil Mysliwiec
99b5a5bbfe Merge pull request #14171 from nestjs/dependabot/npm_and_yarn/sample/11-swagger/cross-spawn-7.0.6
chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /sample/11-swagger
2024-11-20 10:39:37 +01:00
Kamil Mysliwiec
1531e6a767 Merge pull request #14172 from nestjs/dependabot/npm_and_yarn/sample/35-use-esm-package-after-node22/multi-c29acd3782
chore(deps): bump cookie and @nestjs/platform-express in /sample/35-use-esm-package-after-node22
2024-11-20 10:36:36 +01:00
dependabot[bot]
3fa80a9dab chore(deps): bump cookie and @nestjs/platform-express
Bumps [cookie](https://github.com/jshttp/cookie) to 0.7.1 and updates ancestor dependency [@nestjs/platform-express](https://github.com/nestjs/nest/tree/HEAD/packages/platform-express). These dependencies need to be updated together.


Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `@nestjs/platform-express` from 10.4.4 to 10.4.8
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v10.4.8/packages/platform-express)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: "@nestjs/platform-express"
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-20 09:27:05 +00:00
Kamil Mysliwiec
439516d7dc Merge pull request #14051 from micalevisk/add-esm-only-sample
chore: add sample on how to use esm on cjs
2024-11-20 10:26:00 +01:00
dependabot[bot]
22c4e9cae3 chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /sample/11-swagger
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-20 09:22:40 +00:00
Kamil Mysliwiec
2dda578d9a Merge pull request #14170 from nestjs/dependabot/npm_and_yarn/sample/24-serve-static/cross-spawn-7.0.6
chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /sample/24-serve-static
2024-11-20 10:21:33 +01:00
Kamil Myśliwiec
b65c41c5a8 fix: use proxy, drain buffer before execution 2024-11-20 10:21:10 +01:00
dependabot[bot]
f0c3cef7d4 chore(deps): bump cross-spawn in /sample/24-serve-static
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-20 08:24:47 +00:00
Kamil Mysliwiec
e2e70c1bc3 Merge pull request #14169 from nestjs/dependabot/npm_and_yarn/sample/15-mvc/cross-spawn-7.0.6
chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /sample/15-mvc
2024-11-20 09:23:39 +01:00
dependabot[bot]
70c18e9e7b chore(deps-dev): bump @typescript-eslint/parser from 8.14.0 to 8.15.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.14.0 to 8.15.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.15.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-20 07:42:35 +00:00
dependabot[bot]
102718ad1b chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /sample/15-mvc
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-20 07:42:00 +00:00
Kamil Mysliwiec
02d26a6fd7 Merge pull request #14164 from nestjs/dependabot/npm_and_yarn/sample/01-cats-app/cross-spawn-7.0.6
chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /sample/01-cats-app
2024-11-20 08:40:13 +01:00
Kamil Mysliwiec
8279fb5319 Merge pull request #14167 from nestjs/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-8.15.0
chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.14.0 to 8.15.0
2024-11-20 08:40:05 +01:00
dependabot[bot]
d4e5743c9b chore(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.14.0 to 8.15.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.15.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-19 00:49:10 +00:00
dependabot[bot]
b1e33ece5d chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /sample/01-cats-app
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 22:46:40 +00:00
Micael Levi L. Cavalcante
e15d73190d chore: add sample on how to use esm with nodejs experimental flag 2024-11-18 11:30:39 -04:00
Kamil Myśliwiec
4564503936 fix: use valid reference for the cancel event constant 2024-11-18 15:27:45 +01:00
Kamil Myśliwiec
30a6b52c50 fix(microservices): grpc client streaming bugs #14094 #13818 2024-11-18 15:23:44 +01:00
Kamil Mysliwiec
bc19f81729 Merge pull request #14161 from nestjs/dependabot/npm_and_yarn/sample/25-dynamic-modules/cross-spawn-7.0.5
chore(deps): bump cross-spawn from 7.0.3 to 7.0.5 in /sample/25-dynamic-modules
2024-11-18 09:50:43 +01:00
dependabot[bot]
989075e6c1 chore(deps): bump cross-spawn in /sample/25-dynamic-modules
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.5.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 08:17:48 +00:00
Kamil Mysliwiec
c60428a6fd Merge pull request #14159 from nestjs/dependabot/npm_and_yarn/sample/33-graphql-mercurius/cross-spawn-7.0.5
chore(deps): bump cross-spawn from 7.0.3 to 7.0.5 in /sample/33-graphql-mercurius
2024-11-18 09:16:06 +01:00
Kamil Mysliwiec
92e2db8d99 Merge pull request #13632 from nestjs/renovate/npm-mysql2-vulnerability
fix(deps): update dependency mysql2 to v3.9.8 [security]
2024-11-18 08:16:45 +01:00
dependabot[bot]
5911ed245e chore(deps): bump cross-spawn in /sample/33-graphql-mercurius
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.5.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 07:15:19 +00:00
Kamil Mysliwiec
1b4decf75e Merge pull request #14151 from nestjs/dependabot/npm_and_yarn/sample/27-scheduling/cross-spawn-7.0.5
chore(deps): bump cross-spawn from 7.0.3 to 7.0.5 in /sample/27-scheduling
2024-11-18 08:13:39 +01:00
Kamil Mysliwiec
36756dd194 Merge pull request #14153 from micalevisk/patch-1
chore: shallow cloning `wrk` repository on benchmark setup
2024-11-18 08:13:18 +01:00
Kamil Mysliwiec
27193cc99f Merge pull request #14155 from webdiscus/cli-color-to-ansis
build(sample): replace cli-color with smaller and faster ansis
2024-11-18 08:13:08 +01:00
Micael Levi L. Cavalcante
194732f4aa chore: shallow cloning wrk repository on benchmarking setup 2024-11-17 12:41:46 -04:00
biodiscus
3559e0c1f2 build(sample): replace cli-color with smaller and faster ansis 2024-11-17 17:01:47 +01:00
dependabot[bot]
2e4ebe4870 chore(deps): bump cross-spawn in /sample/27-scheduling
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.5.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-16 21:17:27 +00:00
Kamil Mysliwiec
7d62aad5fc Merge pull request #14146 from nestjs/dependabot/npm_and_yarn/sample/28-sse/multi-c29acd3782
chore(deps): bump cookie and @nestjs/platform-express in /sample/28-sse
2024-11-16 22:16:21 +01:00
Kamil Mysliwiec
ef8f64cf42 Merge pull request #14149 from nestjs/dependabot/npm_and_yarn/sample/32-graphql-federation-schema-first/gateway/cross-spawn-7.0.5
chore(deps): bump cross-spawn from 7.0.3 to 7.0.5 in /sample/32-graphql-federation-schema-first/gateway
2024-11-16 22:16:13 +01:00
Kamil Mysliwiec
afa667509f Merge pull request #12995 from nestjs/renovate/supertest-6.x
chore(deps): update dependency supertest to v6.3.4
2024-11-16 22:15:59 +01:00
renovate[bot]
dc09280634 chore(deps): update dependency supertest to v6.3.4 2024-11-16 20:31:33 +00:00
dependabot[bot]
8115930ec8 chore(deps): bump cookie and @nestjs/platform-express in /sample/28-sse
Bumps [cookie](https://github.com/jshttp/cookie) to 0.7.1 and updates ancestor dependency [@nestjs/platform-express](https://github.com/nestjs/nest/tree/HEAD/packages/platform-express). These dependencies need to be updated together.


Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `@nestjs/platform-express` from 10.4.4 to 10.4.8
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v10.4.8/packages/platform-express)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: "@nestjs/platform-express"
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-16 20:27:58 +00:00
Kamil Mysliwiec
8fc1c9786a Merge pull request #12815 from nestjs/renovate/node-20.x
chore(deps): update dependency @types/node to v20.17.6
2024-11-16 21:26:57 +01:00
renovate[bot]
2b9f2fd493 chore(deps): update dependency @types/node to v20.17.6 2024-11-16 20:20:53 +00:00
renovate[bot]
2867dff697 fix(deps): update dependency mysql2 to v3.9.8 [security] 2024-11-16 20:18:43 +00:00
dependabot[bot]
ee7eb9d48d chore(deps): bump cross-spawn
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.5.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-16 20:13:01 +00:00
Kamil Mysliwiec
f38a07719a Merge pull request #14147 from nestjs/chore/upgrade-deps
chore: upgrade deps
2024-11-16 21:11:51 +01:00
Kamil Myśliwiec
af8cd7e4c9 sample: update grpc example 2024-11-16 20:49:36 +01:00
Kamil Myśliwiec
c800b06a22 ci: update node version for eslint workflow 2024-11-16 20:38:48 +01:00
Kamil Myśliwiec
f00192f4f1 chore: regenerate package-lock 2024-11-16 20:32:25 +01:00
Kamil Myśliwiec
b8ddf4fa59 chore: revert markdown-table 2024-11-16 20:29:41 +01:00
Kamil Myśliwiec
a9954025dd style(lint): update eslint rules 2024-11-16 20:26:21 +01:00
Kamil Myśliwiec
0d9b7c85db sample: updateg grpc options type 2024-11-16 20:25:42 +01:00
Kamil Myśliwiec
d9a592d899 chore: update package json 2024-11-16 20:01:52 +01:00
Kamil Myśliwiec
ef24801fcc chore: upgrade deps 2024-11-16 20:00:50 +01:00
Kamil Mysliwiec
bde210677f Merge pull request #14144 from nestjs/dependabot/npm_and_yarn/sample/33-graphql-mercurius/multi-ba981ebd23
chore(deps): bump cookie, light-my-request and @nestjs/platform-fastify in /sample/33-graphql-mercurius
2024-11-15 15:29:41 +01:00
Kamil Myśliwiec
d35006a0d3 chore: update readmes 2024-11-15 14:55:07 +01:00
dependabot[bot]
29a80ba8ab chore(deps): bump cookie, light-my-request and @nestjs/platform-fastify
Bumps [cookie](https://github.com/jshttp/cookie) to 0.7.2 and updates ancestor dependencies [cookie](https://github.com/jshttp/cookie), [light-my-request](https://github.com/fastify/light-my-request) and [@nestjs/platform-fastify](https://github.com/nestjs/nest/tree/HEAD/packages/platform-fastify). These dependencies need to be updated together.


Updates `cookie` from 0.5.0 to 0.7.2
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.5.0...v0.7.2)

Updates `light-my-request` from 5.11.0 to 5.14.0
- [Release notes](https://github.com/fastify/light-my-request/releases)
- [Commits](https://github.com/fastify/light-my-request/compare/v5.11.0...v5.14.0)

Updates `@nestjs/platform-fastify` from 10.3.2 to 10.4.8
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v10.4.8/packages/platform-fastify)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: light-my-request
  dependency-type: indirect
- dependency-name: "@nestjs/platform-fastify"
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-15 13:53:10 +00:00
Kamil Myśliwiec
3c5180d2d5 chore(@nestjs) publish v10.4.8 release 2024-11-15 14:51:54 +01:00
Kamil Mysliwiec
a7b73e3107 Merge pull request #14143 from nestjs/feat/expose-listening-stream
feat(core): expose listening stream from http adapter host
2024-11-15 14:40:16 +01:00
Kamil Mysliwiec
491ed77f22 Merge pull request #14059 from v-sum/fix-disregarded-rmq-client-options
fix(microservices): include discarded rmq client options
2024-11-15 14:32:46 +01:00
Kamil Myśliwiec
e64ab182ba refactor(core): replace internal init with an inline promise 2024-11-15 14:32:09 +01:00
Kamil Mysliwiec
dade6d5889 Merge pull request #14139 from mksony/chore/ensure-application-init-before-accepting-sigterm
chore(core): defer application shutdown until init finishes
2024-11-15 14:31:03 +01:00
Kamil Mysliwiec
49dc36d9e5 Merge pull request #14132 from nestjs/fix/mqtt-microservices-qos2
fix(microservices): no messages emitted with mqtt when qos set
2024-11-15 14:30:42 +01:00
Kamil Mysliwiec
831d29553d Merge pull request #14133 from nestjs/fix/flaky-durable-provider
fix(core): flaky durable provider, remove instance on error
2024-11-15 14:30:28 +01:00
Kamil Myśliwiec
1a076fc4cb feat(core): expose listening stream from http adapter host 2024-11-15 11:23:16 +01:00
Max Karacsony
5c6986f0c7 chore(core): defer application shutdown until init finishes 2024-11-14 13:50:07 +01:00
Kamil Myśliwiec
229d97f018 fix(core): flaky durable provider, remove instance on error #13953 2024-11-12 13:23:51 +01:00
Kamil Myśliwiec
1fe4dc2cad fix(microservices): no messages emitted with mqtt when qos set #14079 2024-11-12 12:35:25 +01:00
Vasile Sumanschi
9cd43532ae fix(microservices): include discarded rmq client options
all other properties, beside 'connectionOptions', from the socketOptions object ( of type AmqpConnectionManagerSocketOptions )
were being discarded on the creation of the AmqpConnectionManager client
https://github.com/nestjs/nest/blob/master/packages/microservices/external/rmq-url.interface.ts#L47
https://github.com/jwalton/node-amqp-connection-manager/blob/v4.1.14/src/AmqpConnectionManager.ts#L46

https://github.com/nestjs/nest/issues/5788#issuecomment-2373361313
2024-10-09 01:17:27 +03:00
Micael Levi L. Cavalcante
aff30bf63d chore: support skip npm script execution depending on require nodejs 2024-10-02 18:03:17 -04:00
Micael Levi L. Cavalcante
0dafaaa0b9 chore: add sample on how to use esm on cjs 2024-10-01 09:35:05 -04:00
Martin Vyšňovský
3e061e9b14 feat(06-mongoose): add update method 2024-08-14 13:07:18 +02:00
Martin Vyšňovský
d0b2835293 test(06-mongoose): add missing tests for service 2024-08-14 13:03:15 +02:00
Martin Vyšňovský
2da82286d2 test(06-mongoose): refactor tests for service 2024-08-14 13:01:09 +02:00
Martin Vyšňovský
3c6a295037 test(06-mongoose): add missing tests for controller 2024-08-14 12:51:28 +02:00
Martin Vyšňovský
3117b241c1 test(06-mongoose): refactor tests for controller 2024-08-14 12:51:28 +02:00
Martin Vyšňovský
f0527d08fc chore(deps): update @nestjs/mongoose in 06-mongoose sample 2024-08-14 12:24:23 +02:00
cain
0f00a347a6 test: test for emit-batch 2024-03-12 14:58:19 +09:00
cain
14cffb328a fix: modify access modifier 2023-12-21 15:57:49 +09:00
cain
cb8e04dd62 feat: add block wrapping return 2023-12-21 10:26:48 +09:00
cain
069676fd84 feat: emit batch 2023-12-20 16:59:46 +09:00
yansb
c032e83e4d test(sample-01): improved tests 2022-12-01 08:23:18 -03:00
yansb
5c9bcc85ef test(sample-01): added controller create cat unit test 2022-11-30 21:50:16 -03:00
yansb
7ec3eb235d test(sample-01): added service unit tests 2022-11-30 21:14:15 -03:00
288 changed files with 29247 additions and 11974 deletions

View File

@@ -121,7 +121,7 @@ jobs:
lint:
working_directory: ~/nest
docker:
- image: cimg/node:<< pipeline.parameters.maintenance-node-version >>
- image: cimg/node:<< pipeline.parameters.current-node-version >>
steps:
- checkout
- *restore-cache

View File

@@ -17,13 +17,13 @@ module.exports = {
sourceType: 'module',
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-array-constructor': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-wrapper-object-types': 'off',
},
},
{
@@ -34,13 +34,13 @@ module.exports = {
sourceType: 'module',
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-wrapper-object-types': 'off',
},
}
]

1
.gitignore vendored
View File

@@ -50,3 +50,4 @@ build/config\.gypi
.npmrc
pnpm-lock.yaml
/.history

View File

@@ -43,7 +43,10 @@ describe('OnModuleDestroy', () => {
it('should sort modules by distance (topological sort) - DESC order', async () => {
@Injectable()
class BB implements OnModuleDestroy {
onModuleDestroy = Sinon.spy();
public field: string;
async onModuleDestroy() {
this.field = 'b-field';
}
}
@Module({
@@ -54,10 +57,13 @@ describe('OnModuleDestroy', () => {
@Injectable()
class AA implements OnModuleDestroy {
public field: string;
constructor(private bb: BB) {}
onModuleDestroy = Sinon.spy();
}
async onModuleDestroy() {
this.field = this.bb.field + '_a-field';
}
}
@Module({
imports: [B],
providers: [AA],
@@ -72,8 +78,7 @@ describe('OnModuleDestroy', () => {
await app.init();
await app.close();
const aa = module.get(AA);
const bb = module.get(BB);
Sinon.assert.callOrder(aa.onModuleDestroy, bb.onModuleDestroy);
const instance = module.get(AA);
expect(instance.field).to.equal('b-field_a-field');
});
});

View File

@@ -39,39 +39,11 @@ describe('OnModuleInit', () => {
});
it('should sort modules by distance (topological sort) - DESC order', async () => {
@Injectable()
class CC implements OnModuleInit {
public field: string;
async onModuleInit() {
this.field = 'c-field';
}
}
@Module({})
class C {
static forRoot() {
return {
module: C,
global: true,
providers: [
{
provide: CC,
useValue: new CC(),
},
],
exports: [CC],
};
}
}
@Injectable()
class BB implements OnModuleInit {
public field: string;
constructor(private cc: CC) {}
async onModuleInit() {
this.field = this.cc.field + '_b-field';
this.field = 'b-field';
}
}
@@ -96,19 +68,14 @@ describe('OnModuleInit', () => {
})
class A {}
@Module({
imports: [A, C.forRoot()],
})
class AppModule {}
const module = await Test.createTestingModule({
imports: [AppModule],
imports: [A],
}).compile();
const app = module.createNestApplication();
await app.init();
const instance = module.get(AA);
expect(instance.field).to.equal('c-field_b-field_a-field');
expect(instance.field).to.equal('b-field_a-field');
});
});

View File

@@ -1,98 +0,0 @@
import {
Controller,
INestMicroservice,
Injectable,
Module,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import {
AsyncOptions,
ClientTCP,
ClientsModule,
MessagePattern,
MicroserviceOptions,
Payload,
TcpOptions,
Transport,
} from '@nestjs/microservices';
import { expect } from 'chai';
let port: number;
do {
port = Math.round(Math.random() * 10000);
} while (port < 1000);
@Injectable()
class RpcOptionsProvider {
getOptions(): TcpOptions {
return {
transport: Transport.TCP,
options: {
port,
host: '0.0.0.0',
},
};
}
}
@Controller()
class RpcController {
@MessagePattern({ cmd: 'sum' })
sumPayload(@Payload() payload: number[]) {
return payload.reduce((a, b) => a + b, 0);
}
}
@Module({
imports: [
ClientsModule.register([
{
name: 'RPC_CLIENT',
transport: Transport.TCP,
options: {
port,
host: '0.0.0.0',
},
},
]),
],
controllers: [RpcController],
providers: [RpcOptionsProvider],
})
class RpcModule {}
describe('RPC Async transport', () => {
let app: INestMicroservice;
let client: ClientTCP;
beforeEach(async () => {
app = await NestFactory.createMicroservice<
AsyncOptions<MicroserviceOptions>
>(RpcModule, {
logger: false,
inject: [RpcOptionsProvider],
useFactory: (optionsProvider: RpcOptionsProvider) =>
optionsProvider.getOptions(),
});
await app.listen();
client = app.get('RPC_CLIENT', { strict: false });
});
it(`/POST`, done => {
let retData = 0;
client.send({ cmd: 'sum' }, [1, 2, 3, 4, 5]).subscribe({
next: val => (retData += val),
error: done,
complete: () => {
expect(retData).to.eq(15);
done();
},
});
});
afterEach(async () => {
await app.close();
});
});

View File

@@ -24,8 +24,9 @@ export class DisconnectedClientController {
return throwError(() =>
code === 'ECONNREFUSED' ||
code === 'CONN_ERR' ||
code === 'ENOTFOUND' ||
code === 'CONNECTION_REFUSED' ||
error.message === 'Connection is closed.'
error.message.includes('Connection is closed.')
? new RequestTimeoutException('ECONNREFUSED')
: new InternalServerErrorException(),
);

View File

@@ -1,13 +1,13 @@
import { Injectable, Module } from '@nestjs/common';
import { Module, Injectable } from '@nestjs/common';
import { AppController } from './app.controller';
import {
ClientOptions,
ClientsModule,
Transport,
ClientsModuleOptionsFactory,
ClientOptions,
ClientTCP,
RpcException,
Transport,
} from '@nestjs/microservices';
import { AppController } from './app.controller';
import * as fs from 'fs';
import * as path from 'path';

View File

@@ -27,10 +27,14 @@ describe('Durable providers', () => {
tenantId: number,
end: (err?: any) => void,
endpoint = '/durable',
opts: {
forceError: boolean;
} = { forceError: false },
) =>
request(server)
.get(endpoint)
.set({ ['x-tenant-id']: tenantId })
.set({ ['x-tenant-id']: String(tenantId) })
.set({ ['x-force-error']: opts.forceError ? 'true' : 'false' })
.end((err, res) => {
if (err) return end(err);
end(res);
@@ -84,6 +88,23 @@ describe('Durable providers', () => {
);
expect(result.body).deep.equal({ tenantId: '3' });
});
it(`should not cache durable providers that throw errors`, async () => {
let result: request.Response;
result = await new Promise<request.Response>(resolve =>
performHttpCall(10, resolve, '/durable/echo', { forceError: true }),
);
expect(result.statusCode).equal(412);
// The second request should be successful
result = await new Promise<request.Response>(resolve =>
performHttpCall(10, resolve, '/durable/echo'),
);
expect(result.body).deep.equal({ tenantId: '10' });
});
});
after(async () => {

View File

@@ -6,6 +6,8 @@ const tenants = new Map<string, ContextId>();
export class DurableContextIdStrategy implements ContextIdStrategy {
attach(contextId: ContextId, request: Request) {
const tenantId = request.headers['x-tenant-id'] as string;
const forceError = request.headers['x-force-error'] === 'true';
let tenantSubTreeId: ContextId;
if (tenants.has(tenantId)) {
@@ -14,10 +16,18 @@ export class DurableContextIdStrategy implements ContextIdStrategy {
tenantSubTreeId = { id: +tenantId } as ContextId;
tenants.set(tenantId, tenantSubTreeId);
}
const payload: {
tenantId: string;
forceError?: boolean;
} = { tenantId };
if (forceError) {
payload.forceError = true;
}
return {
resolve: (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId : contextId,
payload: { tenantId },
payload,
};
}
}

View File

@@ -1,11 +1,23 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import {
Inject,
Injectable,
PreconditionFailedException,
Scope,
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
@Injectable({ scope: Scope.REQUEST, durable: true })
export class DurableService {
public instanceCounter = 0;
constructor(@Inject(REQUEST) public readonly requestPayload: unknown) {}
constructor(
@Inject(REQUEST)
public readonly requestPayload: { tenantId: string; forceError: boolean },
) {
if (requestPayload.forceError) {
throw new PreconditionFailedException('Forced error');
}
}
greeting() {
++this.instanceCounter;

View File

@@ -218,61 +218,6 @@ describe('WebSocketGateway (WsAdapter)', () => {
);
});
it('should set messageParser by using setMessageParser method', async () => {
const testingModule = await Test.createTestingModule({
providers: [ApplicationGateway],
}).compile();
app = testingModule.createNestApplication();
const wsAdapter = new WsAdapter(app);
wsAdapter.setMessageParser(data => {
const [event, payload] = JSON.parse(data.toString());
return { event, data: payload };
});
app.useWebSocketAdapter(wsAdapter);
await app.listen(3000);
ws = new WebSocket('ws://localhost:8080');
await new Promise(resolve => ws.on('open', resolve));
ws.send(JSON.stringify(['push', { test: 'test' }]));
await new Promise<void>(resolve =>
ws.on('message', data => {
expect(JSON.parse(data).data.test).to.be.eql('test');
ws.close();
resolve();
}),
);
});
it('should set messageParser by using constructor options', async () => {
const testingModule = await Test.createTestingModule({
providers: [ApplicationGateway],
}).compile();
app = testingModule.createNestApplication();
const wsAdapter = new WsAdapter(app, {
messageParser: data => {
const [event, payload] = JSON.parse(data.toString());
return { event, data: payload };
},
});
app.useWebSocketAdapter(wsAdapter);
await app.listen(3000);
ws = new WebSocket('ws://localhost:8080');
await new Promise(resolve => ws.on('open', resolve));
ws.send(JSON.stringify(['push', { test: 'test' }]));
await new Promise<void>(resolve =>
ws.on('message', data => {
expect(JSON.parse(data).data.test).to.be.eql('test');
ws.close();
resolve();
}),
);
});
afterEach(async function () {
await app.close();
});

View File

@@ -3,5 +3,5 @@
"packages": [
"packages/*"
],
"version": "10.4.7"
"version": "10.4.8"
}

13271
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -59,26 +59,25 @@
},
"dependencies": {
"@nuxtjs/opencollective": "0.3.2",
"ansis": "3.3.2",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"cli-color": "2.0.4",
"cors": "2.8.5",
"express": "4.21.1",
"fast-json-stringify": "6.0.0",
"fast-safe-stringify": "2.1.1",
"graphql-subscriptions": "2.0.0",
"iterare": "1.2.1",
"object-hash": "3.0.0",
"path-to-regexp": "3.2.0",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"socket.io": "4.8.0",
"tslib": "2.7.0",
"socket.io": "4.8.1",
"tslib": "2.8.1",
"uid": "2.0.2",
"uuid": "10.0.0"
"uuid": "11.0.3"
},
"devDependencies": {
"@apollo/server": "4.11.0",
"@apollo/server": "4.11.2",
"@codechecks/client": "0.1.12",
"@commitlint/cli": "19.5.0",
"@commitlint/config-angular": "19.5.0",
@@ -88,11 +87,11 @@
"@fastify/multipart": "8.3.0",
"@fastify/static": "7.0.4",
"@fastify/view": "9.1.0",
"@grpc/grpc-js": "1.11.1",
"@grpc/grpc-js": "1.12.2",
"@grpc/proto-loader": "0.7.13",
"@nestjs/apollo": "12.2.0",
"@nestjs/graphql": "12.2.0",
"@nestjs/mongoose": "10.0.10",
"@nestjs/apollo": "12.2.1",
"@nestjs/graphql": "12.2.1",
"@nestjs/mongoose": "10.1.0",
"@nestjs/typeorm": "10.0.2",
"@types/amqplib": "0.10.5",
"@types/bytes": "3.1.4",
@@ -102,16 +101,16 @@
"@types/express": "4.17.21",
"@types/gulp": "4.0.17",
"@types/http-errors": "2.0.4",
"@types/mocha": "10.0.8",
"@types/node": "22.5.5",
"@types/mocha": "10.0.9",
"@types/node": "22.9.0",
"@types/sinon": "17.0.3",
"@types/supertest": "2.0.16",
"@types/ws": "8.5.12",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@types/supertest": "6.0.2",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "8.15.0",
"@typescript-eslint/parser": "8.15.0",
"amqp-connection-manager": "4.1.14",
"amqplib": "0.10.4",
"artillery": "2.0.20",
"artillery": "2.0.21",
"body-parser": "1.20.3",
"bytes": "3.1.2",
"cache-manager": "5.7.6",
@@ -119,57 +118,58 @@
"chai": "4.5.0",
"chai-as-promised": "7.1.2",
"clang-format": "1.8.0",
"concurrently": "9.0.1",
"concurrently": "9.1.0",
"conventional-changelog": "6.0.0",
"core-js": "3.38.1",
"core-js": "3.39.0",
"coveralls": "3.1.1",
"delete-empty": "3.0.0",
"engine.io-client": "6.6.1",
"engine.io-client": "6.6.2",
"eslint": "8.57.1",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-import": "2.31.0",
"eventsource": "2.0.2",
"fancy-log": "2.0.0",
"fastify": "4.28.1",
"graphql": "16.9.0",
"graphql-tools": "9.0.1",
"gulp": "4.0.2",
"graphql-tools": "9.0.3",
"graphql-subscriptions": "2.0.0",
"gulp": "5.0.0",
"gulp-clang-format": "1.0.27",
"gulp-clean": "0.4.0",
"gulp-sourcemaps": "3.0.0",
"gulp-typescript": "5.0.1",
"gulp-watch": "5.0.1",
"http-errors": "2.0.0",
"husky": "9.1.5",
"husky": "9.1.6",
"imports-loader": "5.0.0",
"ioredis": "5.4.1",
"json-loader": "0.5.7",
"kafkajs": "2.2.4",
"lerna": "2.11.0",
"lerna-changelog": "2.2.0",
"light-my-request": "6.1.0",
"light-my-request": "6.3.0",
"lint-staged": "15.2.10",
"markdown-table": "2.0.0",
"mocha": "10.7.3",
"mongoose": "8.6.2",
"mqtt": "5.6.0",
"mocha": "10.8.2",
"mongoose": "8.8.1",
"mqtt": "5.10.2",
"multer": "1.4.4",
"mysql2": "3.11.3",
"mysql2": "3.11.4",
"nats": "2.28.2",
"nodemon": "3.1.5",
"nyc": "14.1.1",
"nodemon": "3.1.7",
"nyc": "17.1.0",
"prettier": "3.3.3",
"redis": "4.7.0",
"rxjs-compat": "6.6.7",
"sinon": "19.0.2",
"sinon-chai": "3.7.0",
"socket.io-client": "4.8.0",
"sinon-chai": "4.0.0",
"socket.io-client": "4.8.1",
"subscriptions-transport-ws": "0.11.0",
"supertest": "7.0.0",
"ts-morph": "23.0.0",
"ts-morph": "24.0.0",
"ts-node": "10.9.2",
"typeorm": "0.3.20",
"typescript": "5.6.2",
"typescript": "5.6.3",
"wrk": "1.2.1",
"ws": "8.18.0"
},

View File

@@ -94,7 +94,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td><a href="https://www.mercedes-benz.com/" target="_blank"><img src="https://nestjs.com/img/logos/mercedes-logo.png" width="100" valign="middle" /></a></td>
<td><a href="https://www.dinii.jp/" target="_blank"><img src="https://nestjs.com/img/logos/dinii-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://bloodycase.com/?promocode=NEST" target="_blank"><img src="https://nestjs.com/img/logos/bloodycase-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-logo.svg" width="150" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-dark-logo.svg#2" width="150" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
</tr>

View File

@@ -3,7 +3,6 @@ import {
PROPERTY_DEPS_METADATA,
SELF_DECLARED_DEPS_METADATA,
} from '../../constants';
import { ForwardReference, InjectionToken } from '../../interfaces';
import { isUndefined } from '../../utils/shared.utils';
/**
@@ -35,8 +34,8 @@ import { isUndefined } from '../../utils/shared.utils';
*
* @publicApi
*/
export function Inject(
token?: InjectionToken | ForwardReference,
export function Inject<T = any>(
token?: T,
): PropertyDecorator & ParameterDecorator {
const injectCallHasArguments = arguments.length > 0;

View File

@@ -16,8 +16,12 @@ export type ParamDecoratorEnhancer = ParameterDecorator;
*
* @publicApi
*/
export function createParamDecorator<FactoryData = any, FactoryOutput = any>(
factory: CustomParamFactory<FactoryData, FactoryOutput>,
export function createParamDecorator<
FactoryData = any,
FactoryInput = any,
FactoryOutput = any,
>(
factory: CustomParamFactory<FactoryData, FactoryInput, FactoryOutput>,
enhancers: ParamDecoratorEnhancer[] = [],
): (
...dataOrPipes: (Type<PipeTransform> | PipeTransform | FactoryData)[]

View File

@@ -118,66 +118,3 @@ export const All = createMappingDecorator(RequestMethod.ALL);
* @publicApi
*/
export const Search = createMappingDecorator(RequestMethod.SEARCH);
/**
* Route handler (method) Decorator. Routes Webdav PROPFIND requests to the specified path.
*
* @see [Routing](https://docs.nestjs.com/controllers#routing)
*
* @publicApi
*/
export const Propfind = createMappingDecorator(RequestMethod.PROPFIND);
/**
* Route handler (method) Decorator. Routes Webdav PROPPATCH requests to the specified path.
*
* @see [Routing](https://docs.nestjs.com/controllers#routing)
*
* @publicApi
*/
export const Proppatch = createMappingDecorator(RequestMethod.PROPPATCH);
/**
* Route handler (method) Decorator. Routes Webdav MKCOL requests to the specified path.
*
* @see [Routing](https://docs.nestjs.com/controllers#routing)
*
* @publicApi
*/
export const Mkcol = createMappingDecorator(RequestMethod.MKCOL);
/**
* Route handler (method) Decorator. Routes Webdav COPY requests to the specified path.
*
* @see [Routing](https://docs.nestjs.com/controllers#routing)
*
* @publicApi
*/
export const Copy = createMappingDecorator(RequestMethod.COPY);
/**
* Route handler (method) Decorator. Routes Webdav MOVE requests to the specified path.
*
* @see [Routing](https://docs.nestjs.com/controllers#routing)
*
* @publicApi
*/
export const Move = createMappingDecorator(RequestMethod.MOVE);
/**
* Route handler (method) Decorator. Routes Webdav LOCK requests to the specified path.
*
* @see [Routing](https://docs.nestjs.com/controllers#routing)
*
* @publicApi
*/
export const Lock = createMappingDecorator(RequestMethod.LOCK);
/**
* Route handler (method) Decorator. Routes Webdav UNLOCK requests to the specified path.
*
* @see [Routing](https://docs.nestjs.com/controllers#routing)
*
* @publicApi
*/
export const Unlock = createMappingDecorator(RequestMethod.UNLOCK);

View File

@@ -13,9 +13,6 @@ export enum HttpStatus {
NO_CONTENT = 204,
RESET_CONTENT = 205,
PARTIAL_CONTENT = 206,
MULTI_STATUS = 207,
ALREADY_REPORTED = 208,
CONTENT_DIFFERENT = 210,
AMBIGUOUS = 300,
MOVED_PERMANENTLY = 301,
FOUND = 302,
@@ -44,17 +41,13 @@ export enum HttpStatus {
I_AM_A_TEAPOT = 418,
MISDIRECTED = 421,
UNPROCESSABLE_ENTITY = 422,
LOCKED = 423,
FAILED_DEPENDENCY = 424,
PRECONDITION_REQUIRED = 428,
TOO_MANY_REQUESTS = 429,
UNRECOVERABLE_ERROR = 456,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
HTTP_VERSION_NOT_SUPPORTED = 505,
INSUFFICIENT_STORAGE = 507,
LOOP_DETECTED = 508,
}

View File

@@ -8,11 +8,4 @@ export enum RequestMethod {
OPTIONS,
HEAD,
SEARCH,
PROPFIND,
PROPPATCH,
MKCOL,
COPY,
MOVE,
LOCK,
UNLOCK,
}

View File

@@ -2,7 +2,7 @@ import {
HttpExceptionBody,
HttpExceptionBodyMessage,
} from '../interfaces/http/http-exception-body.interface';
import { isNumber, isObject, isString } from '../utils/shared.utils';
import { isObject, isString } from '../utils/shared.utils';
export interface HttpExceptionOptions {
/** original cause of the error */
@@ -115,14 +115,17 @@ export class HttpException extends Error {
message: HttpExceptionBodyMessage,
statusCode: number,
): HttpExceptionBody;
public static createBody(
message: HttpExceptionBodyMessage,
error: string,
statusCode: number,
): HttpExceptionBody;
public static createBody<Body extends Record<string, unknown>>(
custom: Body,
): Body;
public static createBody<Body extends Record<string, unknown>>(
arg0: null | HttpExceptionBodyMessage | Body,
arg1?: HttpExceptionBodyMessage | string,
@@ -135,7 +138,7 @@ export class HttpException extends Error {
};
}
if (isString(arg0) || Array.isArray(arg0) || isNumber(arg0)) {
if (isString(arg0) || Array.isArray(arg0)) {
return {
message: arg0,
error: arg1 as string,

View File

@@ -1,9 +1,9 @@
import { Readable } from 'stream';
import { types } from 'util';
import { HttpStatus } from '../enums';
import { Logger } from '../services';
import { isFunction } from '../utils/shared.utils';
import { StreamableFileOptions, StreamableHandlerResponse } from './interfaces';
import { Logger } from '../services';
/**
* @see [Streaming files](https://docs.nestjs.com/techniques/streaming-files)
@@ -31,7 +31,7 @@ export class StreamableFile {
};
protected logError: (err: Error) => void = (err: Error) => {
this.logger.error(err);
this.logger.error(err.message, err.stack);
};
constructor(buffer: Uint8Array, options?: StreamableFileOptions);

View File

@@ -2,7 +2,7 @@ import { Type } from '../type.interface';
import { ClassTransformOptions } from './class-transform-options.interface';
export interface TransformerPackage {
plainToInstance<T>(
plainToClass<T>(
cls: Type<T>,
plain: unknown,
options?: ClassTransformOptions,

View File

@@ -1,9 +1,7 @@
import { ExecutionContext } from './execution-context.interface';
/**
* @publicApi
*/
export type CustomParamFactory<TData = any, TOutput = any> = (
export type CustomParamFactory<TData = any, TInput = any, TOutput = any> = (
data: TData,
context: ExecutionContext,
input: TInput,
) => TOutput;

View File

@@ -1,4 +1,4 @@
export type HttpExceptionBodyMessage = string | string[] | number;
export type HttpExceptionBodyMessage = string | string[];
export interface HttpExceptionBody {
message: HttpExceptionBodyMessage;

View File

@@ -47,20 +47,6 @@ export interface HttpServer<
put(path: string, handler: RequestHandler<TRequest, TResponse>): any;
patch(handler: RequestHandler<TRequest, TResponse>): any;
patch(path: string, handler: RequestHandler<TRequest, TResponse>): any;
propfind?(handler: RequestHandler<TRequest, TResponse>): any;
propfind?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
proppatch?(handler: RequestHandler<TRequest, TResponse>): any;
proppatch?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
mkcol?(handler: RequestHandler<TRequest, TResponse>): any;
mkcol?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
copy?(handler: RequestHandler<TRequest, TResponse>): any;
copy?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
move?(handler: RequestHandler<TRequest, TResponse>): any;
move?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
lock?(handler: RequestHandler<TRequest, TResponse>): any;
lock?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
unlock?(handler: RequestHandler<TRequest, TResponse>): any;
unlock?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
all(path: string, handler: RequestHandler<TRequest, TResponse>): any;
all(handler: RequestHandler<TRequest, TResponse>): any;
options(handler: RequestHandler<TRequest, TResponse>): any;

View File

@@ -35,6 +35,7 @@ export interface ModuleMetadata {
*/
exports?: Array<
| DynamicModule
| Promise<DynamicModule>
| string
| symbol
| Provider

View File

@@ -44,13 +44,4 @@ export class NestApplicationContextOptions {
* @default false
*/
snapshot?: boolean;
/**
* Determines what algorithm use to generate module ids.
* When set to `deep-hash`, the module id is generated based on the serialized module definition.
* When set to `reference`, each module obtains a unique id based on its reference.
*
* @default 'reference'
*/
moduleIdGeneratorAlgorithm?: 'deep-hash' | 'reference';
}

View File

@@ -1,11 +1,8 @@
import { ShutdownSignal } from '../enums/shutdown-signal.enum';
import { LoggerService, LogLevel } from '../services/logger.service';
import { DynamicModule } from './modules';
import { NestApplicationContextOptions } from './nest-application-context-options.interface';
import { Type } from './type.interface';
export type SelectOptions = Pick<NestApplicationContextOptions, 'abortOnError'>;
export interface GetOrResolveOptions {
/**
* If enabled, lookup will only be performed in the host module.
@@ -30,10 +27,7 @@ export interface INestApplicationContext {
* Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module.
* @returns {INestApplicationContext}
*/
select<T>(
module: Type<T> | DynamicModule,
options?: SelectOptions,
): INestApplicationContext;
select<T>(module: Type<T> | DynamicModule): INestApplicationContext;
/**
* Retrieves an instance of either injectable or controller, otherwise, throws exception.

View File

@@ -1,4 +1,3 @@
import { Observable } from 'rxjs';
import { ExceptionFilter } from './exceptions/exception-filter.interface';
import { CanActivate } from './features/can-activate.interface';
import { NestInterceptor } from './features/nest-interceptor.interface';
@@ -20,8 +19,8 @@ export interface INestMicroservice extends INestApplicationContext {
listen(): Promise<any>;
/**
* Registers a web socket adapter that will be used for Gateways.
* Use to override the default `socket.io` library.
* Register Ws Adapter which will be used inside Gateways.
* Use when you want to override default `socket.io` library.
*
* @param {WebSocketAdapter} adapter
* @returns {this}
@@ -29,64 +28,37 @@ export interface INestMicroservice extends INestApplicationContext {
useWebSocketAdapter(adapter: WebSocketAdapter): this;
/**
* Registers global exception filters (will be used for every pattern handler).
* Registers exception filters as global filters (will be used within every message pattern handler)
*
* @param {...ExceptionFilter} filters
*/
useGlobalFilters(...filters: ExceptionFilter[]): this;
/**
* Registers global pipes (will be used for every pattern handler).
* Registers pipes as global pipes (will be used within every message pattern handler)
*
* @param {...PipeTransform} pipes
*/
useGlobalPipes(...pipes: PipeTransform<any>[]): this;
/**
* Registers global interceptors (will be used for every pattern handler).
* Registers interceptors as global interceptors (will be used within every message pattern handler)
*
* @param {...NestInterceptor} interceptors
*/
useGlobalInterceptors(...interceptors: NestInterceptor[]): this;
/**
* Registers global guards (will be used for every pattern handler).
* Registers guards as global guards (will be used within every message pattern handler)
*
* @param {...CanActivate} guards
*/
useGlobalGuards(...guards: CanActivate[]): this;
/**
* Terminates the application.
* Terminates the application
*
* @returns {Promise<void>}
*/
close(): Promise<void>;
/**
* Returns an observable that emits status changes.
*
* @returns {Observable<string>}
*/
status: Observable<string>;
/**
* Registers an event listener for the given event.
* @param event Event name
* @param callback Callback to be executed when the event is emitted
*/
on<
EventsMap extends Record<string, Function> = Record<string, Function>,
EventKey extends keyof EventsMap = keyof EventsMap,
EventCallback extends EventsMap[EventKey] = EventsMap[EventKey],
>(
event: EventKey,
callback: EventCallback,
): void;
/**
* Returns an instance of the underlying server/broker instance,
* or a group of servers if there are more than one.
*/
unwrap<T>(): T;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/common",
"version": "10.4.7",
"version": "10.4.8",
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
"author": "Kamil Mysliwiec",
"homepage": "https://nestjs.com",

View File

@@ -1,10 +1,9 @@
export * from './default-value.pipe';
export * from './file';
export * from './parse-array.pipe';
export * from './parse-bool.pipe';
export * from './parse-date.pipe';
export * from './parse-enum.pipe';
export * from './parse-float.pipe';
export * from './parse-int.pipe';
export * from './parse-float.pipe';
export * from './parse-enum.pipe';
export * from './parse-uuid.pipe';
export * from './validation.pipe';
export * from './file';

View File

@@ -7,7 +7,7 @@ import {
PipeTransform,
} from '../interfaces/features/pipe-transform.interface';
import { HttpErrorByCode } from '../utils/http-error-by-code.util';
import { isNil, isString, isUndefined } from '../utils/shared.utils';
import { isNil, isUndefined, isString } from '../utils/shared.utils';
import { ValidationPipe, ValidationPipeOptions } from './validation.pipe';
const VALIDATION_ERROR_MESSAGE = 'Validation failed (parsable array expected)';
@@ -21,26 +21,9 @@ export interface ParseArrayOptions
ValidationPipeOptions,
'transform' | 'validateCustomDecorators' | 'exceptionFactory'
> {
/**
* Type for items to be converted into
*/
items?: Type<unknown>;
/**
* Items separator to split string by
* @default ','
*/
separator?: string;
/**
* If true, the pipe will return null or undefined if the value is not provided
* @default false
*/
optional?: boolean;
/**
* A factory function that returns an exception object to be thrown
* if validation fails.
* @param error Error message or object
* @returns The exception object
*/
exceptionFactory?: (error: any) => any;
}

View File

@@ -15,21 +15,8 @@ import { isNil } from '../utils/shared.utils';
* @publicApi
*/
export interface ParseBoolPipeOptions {
/**
* The HTTP status code to be used in the response when the validation fails.
*/
errorHttpStatusCode?: ErrorHttpStatusCode;
/**
* A factory function that returns an exception object to be thrown
* if validation fails.
* @param error Error message
* @returns The exception object
*/
exceptionFactory?: (error: string) => any;
/**
* If true, the pipe will return null or undefined if the value is not provided
* @default false
*/
optional?: boolean;
}

View File

@@ -1,74 +0,0 @@
import { Injectable } from '../decorators/core/injectable.decorator';
import { HttpStatus } from '../enums/http-status.enum';
import { PipeTransform } from '../interfaces/features/pipe-transform.interface';
import {
ErrorHttpStatusCode,
HttpErrorByCode,
} from '../utils/http-error-by-code.util';
import { isNil } from '../utils/shared.utils';
export interface ParseDatePipeOptions {
/**
* If true, the pipe will return null or undefined if the value is not provided
* @default false
*/
optional?: boolean;
/**
* Default value for the date
*/
default?: () => Date;
/**
* The HTTP status code to be used in the response when the validation fails.
*/
errorHttpStatusCode?: ErrorHttpStatusCode;
/**
* A factory function that returns an exception object to be thrown
* if validation fails.
* @param error Error message
* @returns The exception object
*/
exceptionFactory?: (error: string) => any;
}
@Injectable()
export class ParseDatePipe
implements PipeTransform<string | number | undefined | null>
{
protected exceptionFactory: (error: string) => any;
constructor(private readonly options: ParseDatePipeOptions = {}) {
const { exceptionFactory, errorHttpStatusCode = HttpStatus.BAD_REQUEST } =
options;
this.exceptionFactory =
exceptionFactory ||
(error => new HttpErrorByCode[errorHttpStatusCode](error));
}
/**
* Method that accesses and performs optional transformation on argument for
* in-flight requests.
*
* @param value currently processed route argument
* @param metadata contains metadata about the currently processed route argument
*/
transform(value: string | number | undefined | null): Date {
if (this.options.optional && isNil(value)) {
return this.options.default
? this.options.default()
: (value as undefined | null);
}
if (!value) {
throw this.exceptionFactory('Validation failed (no Date provided)');
}
const transformedValue = new Date(value);
if (isNaN(transformedValue.getTime())) {
throw this.exceptionFactory('Validation failed (invalid date format)');
}
return transformedValue;
}
}

View File

@@ -11,21 +11,8 @@ import { isNil } from '../utils/shared.utils';
* @publicApi
*/
export interface ParseEnumPipeOptions {
/**
* If true, the pipe will return null or undefined if the value is not provided
* @default false
*/
optional?: boolean;
/**
* The HTTP status code to be used in the response when the validation fails.
*/
errorHttpStatusCode?: ErrorHttpStatusCode;
/**
* A factory function that returns an exception object to be thrown
* if validation fails.
* @param error Error message
* @returns The exception object
*/
exceptionFactory?: (error: string) => any;
}

View File

@@ -11,21 +11,8 @@ import { isNil } from '../utils/shared.utils';
* @publicApi
*/
export interface ParseFloatPipeOptions {
/**
* The HTTP status code to be used in the response when the validation fails.
*/
errorHttpStatusCode?: ErrorHttpStatusCode;
/**
* A factory function that returns an exception object to be thrown
* if validation fails.
* @param error Error message
* @returns The exception object
*/
exceptionFactory?: (error: string) => any;
/**
* If true, the pipe will return null or undefined if the value is not provided
* @default false
*/
optional?: boolean;
}

View File

@@ -15,21 +15,8 @@ import { isNil } from '../utils/shared.utils';
* @publicApi
*/
export interface ParseIntPipeOptions {
/**
* The HTTP status code to be used in the response when the validation fails.
*/
errorHttpStatusCode?: ErrorHttpStatusCode;
/**
* A factory function that returns an exception object to be thrown
* if validation fails.
* @param error Error message
* @returns The exception object
*/
exceptionFactory?: (error: string) => any;
/**
* If true, the pipe will return null or undefined if the value is not provided
* @default false
*/
optional?: boolean;
}

View File

@@ -15,25 +15,9 @@ import { isNil, isString } from '../utils/shared.utils';
* @publicApi
*/
export interface ParseUUIDPipeOptions {
/**
* UUID version to validate
*/
version?: '3' | '4' | '5' | '7';
/**
* The HTTP status code to be used in the response when the validation fails.
*/
errorHttpStatusCode?: ErrorHttpStatusCode;
/**
* A factory function that returns an exception object to be thrown
* if validation fails.
* @param error Error message
* @returns The exception object
*/
exceptionFactory?: (errors: string) => any;
/**
* If true, the pipe will return null or undefined if the value is not provided
* @default false
*/
optional?: boolean;
}

View File

@@ -121,7 +121,7 @@ export class ValidationPipe implements PipeTransform<any> {
const isNil = value !== originalValue;
const isPrimitive = this.isPrimitive(value);
this.stripProtoKeys(value);
let entity = classTransformer.plainToInstance(
let entity = classTransformer.plainToClass(
metatype,
value,
this.transformOptions,
@@ -203,12 +203,6 @@ export class ValidationPipe implements PipeTransform<any> {
return value === true || value === 'true';
}
if (metatype === Number) {
if (isUndefined(value)) {
// This is a workaround to deal with optional numeric values since
// optional numerics shouldn't be parsed to a valid number when
// they were not defined
return undefined;
}
return +value;
}
return value;

View File

@@ -94,11 +94,7 @@ export class ClassSerializerInterceptor implements NestInterceptor {
if (plainOrClass instanceof options.type) {
return classTransformer.classToPlain(plainOrClass, options);
}
const instance = classTransformer.plainToInstance(
options.type,
plainOrClass,
options,
);
const instance = classTransformer.plainToClass(options.type, plainOrClass);
return classTransformer.classToPlain(instance, options);
}

View File

@@ -1,4 +1,3 @@
import { inspect, InspectOptions } from 'util';
import { Injectable, Optional } from '../decorators/core';
import { clc, yellow } from '../utils/cli-colors.util';
import {
@@ -10,8 +9,6 @@ import {
import { LoggerService, LogLevel } from './logger.service';
import { isLogLevelEnabled } from './utils';
const DEFAULT_DEPTH = 5;
export interface ConsoleLoggerOptions {
/**
* Enabled log levels.
@@ -19,73 +16,8 @@ export interface ConsoleLoggerOptions {
logLevels?: LogLevel[];
/**
* If enabled, will print timestamp (time difference) between current and previous log message.
* Note: This option is not used when `json` is enabled.
*/
timestamp?: boolean;
/**
* A prefix to be used for each log message.
* Note: This option is not used when `json` is enabled.
*/
prefix?: string;
/**
* If enabled, will print the log message in JSON format.
*/
json?: boolean;
/**
* If enabled, will print the log message in color.
* Default true if json is disabled, false otherwise
*/
colors?: boolean;
/**
* The context of the logger.
*/
context?: string;
/**
* If enabled, will print the log message in a single line, even if it is an object with multiple properties.
* If set to a number, the most n inner elements are united on a single line as long as all properties fit into breakLength. Short array elements are also grouped together.
* Default true when `json` is enabled, false otherwise.
*/
compact?: boolean | number;
/**
* Specifies the maximum number of Array, TypedArray, Map, Set, WeakMap, and WeakSet elements to include when formatting.
* Set to null or Infinity to show all elements. Set to 0 or negative to show no elements.
* Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output.
* @default 100
*/
maxArrayLength?: number;
/**
* Specifies the maximum number of characters to include when formatting.
* Set to null or Infinity to show all elements. Set to 0 or negative to show no characters.
* Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output.
* @default 10000.
*/
maxStringLength?: number;
/**
* If enabled, will sort keys while formatting objects.
* Can also be a custom sorting function.
* Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output.
* @default false
*/
sorted?: boolean | ((a: string, b: string) => number);
/**
* Specifies the number of times to recurse while formatting object. T
* This is useful for inspecting large objects. To recurse up to the maximum call stack size pass Infinity or null.
* Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output.
* @default 5
*/
depth?: number;
/**
* If true, object's non-enumerable symbols and properties are included in the formatted result.
* WeakMap and WeakSet entries are also included as well as user defined prototype properties
* @default false
*/
showHidden?: boolean;
/**
* The length at which input values are split across multiple lines. Set to Infinity to format the input as a single line (in combination with "compact" set to true).
* Default Infinity when "compact" is true, 80 otherwise.
* Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output.
*/
breakLength?: number;
}
const DEFAULT_LOG_LEVELS: LogLevel[] = [
@@ -108,54 +40,22 @@ const dateTimeFormatter = new Intl.DateTimeFormat(undefined, {
@Injectable()
export class ConsoleLogger implements LoggerService {
/**
* The options of the logger.
*/
protected options: ConsoleLoggerOptions;
/**
* The context of the logger (can be set manually or automatically inferred).
*/
protected context?: string;
/**
* The original context of the logger (set in the constructor).
*/
protected originalContext?: string;
/**
* The options used for the "inspect" method.
*/
protected inspectOptions: InspectOptions;
/**
* The last timestamp at which the log message was printed.
*/
protected static lastTimestampAt?: number;
private static lastTimestampAt?: number;
private originalContext?: string;
constructor();
constructor(context: string);
constructor(options: ConsoleLoggerOptions);
constructor(context: string, options: ConsoleLoggerOptions);
constructor(
@Optional()
contextOrOptions?: string | ConsoleLoggerOptions,
protected context?: string,
@Optional()
options?: ConsoleLoggerOptions,
protected options: ConsoleLoggerOptions = {},
) {
// eslint-disable-next-line prefer-const
let [context, opts] = isString(contextOrOptions)
? [contextOrOptions, options]
: !!options
? [undefined, options]
: [contextOrOptions?.context, contextOrOptions];
opts = opts ?? {};
opts.logLevels ??= DEFAULT_LOG_LEVELS;
opts.colors ??= opts.colors ?? (opts.json ? false : true);
opts.prefix ??= 'Nest';
this.options = opts;
this.inspectOptions = this.getInspectOptions();
if (!options.logLevels) {
options.logLevels = DEFAULT_LOG_LEVELS;
}
if (context) {
this.context = context;
this.originalContext = context;
}
}
@@ -191,7 +91,7 @@ export class ConsoleLogger implements LoggerService {
const { messages, context, stack } =
this.getContextAndStackAndMessagesToPrint([message, ...optionalParams]);
this.printMessages(messages, context, 'error', 'stderr', stack);
this.printMessages(messages, context, 'error', 'stderr');
this.printStackTrace(stack);
}
@@ -303,18 +203,8 @@ export class ConsoleLogger implements LoggerService {
context = '',
logLevel: LogLevel = 'log',
writeStreamType?: 'stdout' | 'stderr',
errorStack?: unknown,
) {
messages.forEach(message => {
if (this.options.json) {
this.printAsJson(message, {
context,
logLevel,
writeStreamType,
errorStack,
});
return;
}
const pidMessage = this.formatPid(process.pid);
const contextMessage = this.formatContext(context);
const timestampDiff = this.updateAndGetTimestampDiff();
@@ -332,57 +222,12 @@ export class ConsoleLogger implements LoggerService {
});
}
protected printAsJson(
message: unknown,
options: {
context: string;
logLevel: LogLevel;
writeStreamType?: 'stdout' | 'stderr';
errorStack?: unknown;
},
) {
type JsonLogObject = {
level: LogLevel;
pid: number;
timestamp: number;
message: unknown;
context?: string;
stack?: unknown;
};
const logObject: JsonLogObject = {
level: options.logLevel,
pid: process.pid,
timestamp: Date.now(),
message,
};
if (options.context) {
logObject.context = options.context;
}
if (options.errorStack) {
logObject.stack = options.errorStack;
}
const formattedMessage =
!this.options.colors && this.inspectOptions.compact === true
? JSON.stringify(logObject, this.stringifyReplacer)
: inspect(logObject, this.inspectOptions);
process[options.writeStreamType ?? 'stdout'].write(`${formattedMessage}\n`);
}
protected formatPid(pid: number) {
return `[${this.options.prefix}] ${pid} - `;
return `[Nest] ${pid} - `;
}
protected formatContext(context: string): string {
if (!context) {
return '';
}
context = `[${context}] `;
return this.options.colors ? yellow(context) : context;
return context ? yellow(`[${context}] `) : '';
}
protected formatMessage(
@@ -411,30 +256,23 @@ export class ConsoleLogger implements LoggerService {
return this.stringifyMessage(message(), logLevel);
}
if (typeof message === 'string') {
return this.colorize(message, logLevel);
}
const outputText = inspect(message, this.inspectOptions);
if (isPlainObject(message)) {
return `Object(${Object.keys(message).length}) ${outputText}`;
}
if (Array.isArray(message)) {
return `Array(${message.length}) ${outputText}`;
}
return outputText;
return isPlainObject(message) || Array.isArray(message)
? `${this.colorize('Object:', logLevel)}\n${JSON.stringify(
message,
(key, value) =>
typeof value === 'bigint' ? value.toString() : value,
2,
)}\n`
: this.colorize(message as string, logLevel);
}
protected colorize(message: string, logLevel: LogLevel) {
if (!this.options.colors || this.options.json) {
return message;
}
const color = this.getColorByLogLevel(logLevel);
return color(message);
}
protected printStackTrace(stack: string) {
if (!stack || this.options.json) {
if (!stack) {
return;
}
process.stderr.write(`${stack}\n`);
@@ -451,58 +289,7 @@ export class ConsoleLogger implements LoggerService {
}
protected formatTimestampDiff(timestampDiff: number) {
const formattedDiff = ` +${timestampDiff}ms`;
return this.options.colors ? yellow(formattedDiff) : formattedDiff;
}
protected getInspectOptions() {
let breakLength = this.options.breakLength;
if (typeof breakLength === 'undefined') {
breakLength = this.options.colors
? this.options.compact
? Infinity
: undefined
: this.options.compact === false
? undefined
: Infinity; // default breakLength to Infinity if inline is not set and colors is false
}
const inspectOptions: InspectOptions = {
depth: this.options.depth ?? DEFAULT_DEPTH,
sorted: this.options.sorted,
showHidden: this.options.showHidden,
compact: this.options.compact ?? (this.options.json ? true : false),
colors: this.options.colors,
breakLength,
};
if (this.options.maxArrayLength) {
inspectOptions.maxArrayLength = this.options.maxArrayLength;
}
if (this.options.maxStringLength) {
inspectOptions.maxStringLength = this.options.maxStringLength;
}
return inspectOptions;
}
protected stringifyReplacer(key: string, value: unknown) {
// Mimic util.inspect behavior for JSON logger with compact on and colors off
if (typeof value === 'bigint') {
return value.toString();
}
if (typeof value === 'symbol') {
return value.toString();
}
if (
value instanceof Map ||
value instanceof Set ||
value instanceof Error
) {
return `${inspect(value, this.inspectOptions)}`;
}
return value;
return yellow(` +${timestampDiff}ms`);
}
private getContextAndMessagesToPrint(args: unknown[]) {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai';
import { GUARDS_METADATA } from '../../constants';
import { applyDecorators, UseGuards } from '../../decorators';
import { GUARDS_METADATA } from '../../constants';
import { CanActivate } from '../../interfaces';
describe('applyDecorators', () => {

View File

@@ -1,22 +1,7 @@
import { expect } from 'chai';
import { Body, HostParam, Param, Query, Search } from '../../decorators';
import { RequestMethod } from '../../enums/request-method.enum';
import {
All,
Delete,
Get,
ParseIntPipe,
Patch,
Post,
Put,
Propfind,
Proppatch,
Mkcol,
Move,
Copy,
Lock,
Unlock,
} from '../../index';
import { All, Delete, Get, ParseIntPipe, Patch, Post, Put } from '../../index';
import { ROUTE_ARGS_METADATA } from '../../constants';
import { RouteParamtypes } from '../../enums/route-paramtypes.enum';
@@ -430,409 +415,3 @@ describe('Inheritance', () => {
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
});
});
describe('@PropFind', () => {
const requestPath = 'test';
const requestProps = {
path: requestPath,
method: RequestMethod.PROPFIND,
};
const requestPathUsingArray = ['foo', 'bar'];
const requestPropsUsingArray = {
path: requestPathUsingArray,
method: RequestMethod.PROPFIND,
};
it('should enhance class with expected request metadata', () => {
class Test {
@Propfind(requestPath)
public static test() {}
@Propfind(requestPathUsingArray)
public static testUsingArray() {}
}
const path = Reflect.getMetadata('path', Test.test);
const method = Reflect.getMetadata('method', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
expect(path).to.be.eql(requestPath);
expect(method).to.be.eql(requestProps.method);
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
});
it('should set path on "/" by default', () => {
class Test {
@Propfind()
public static test(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
@Propfind([])
public static testUsingArray(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
}
const path = Reflect.getMetadata('path', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
expect(path).to.be.eql('/');
expect(pathUsingArray).to.be.eql('/');
});
});
describe('@PropPatch', () => {
const requestPath = 'test';
const requestProps = {
path: requestPath,
method: RequestMethod.PROPPATCH,
};
const requestPathUsingArray = ['foo', 'bar'];
const requestPropsUsingArray = {
path: requestPathUsingArray,
method: RequestMethod.PROPPATCH,
};
it('should enhance class with expected request metadata', () => {
class Test {
@Proppatch(requestPath)
public static test() {}
@Proppatch(requestPathUsingArray)
public static testUsingArray() {}
}
const path = Reflect.getMetadata('path', Test.test);
const method = Reflect.getMetadata('method', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
expect(path).to.be.eql(requestPath);
expect(method).to.be.eql(requestProps.method);
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
});
it('should set path on "/" by default', () => {
class Test {
@Proppatch()
public static test(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
@Proppatch([])
public static testUsingArray(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
}
const path = Reflect.getMetadata('path', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
expect(path).to.be.eql('/');
expect(pathUsingArray).to.be.eql('/');
});
});
describe('@MkCol', () => {
const requestPath = 'test';
const requestProps = {
path: requestPath,
method: RequestMethod.MKCOL,
};
const requestPathUsingArray = ['foo', 'bar'];
const requestPropsUsingArray = {
path: requestPathUsingArray,
method: RequestMethod.MKCOL,
};
it('should enhance class with expected request metadata', () => {
class Test {
@Mkcol(requestPath)
public static test() {}
@Mkcol(requestPathUsingArray)
public static testUsingArray() {}
}
const path = Reflect.getMetadata('path', Test.test);
const method = Reflect.getMetadata('method', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
expect(path).to.be.eql(requestPath);
expect(method).to.be.eql(requestProps.method);
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
});
it('should set path on "/" by default', () => {
class Test {
@Mkcol()
public static test(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
@Mkcol([])
public static testUsingArray(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
}
const path = Reflect.getMetadata('path', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
expect(path).to.be.eql('/');
expect(pathUsingArray).to.be.eql('/');
});
});
describe('@Copy', () => {
const requestPath = 'test';
const requestProps = {
path: requestPath,
method: RequestMethod.COPY,
};
const requestPathUsingArray = ['foo', 'bar'];
const requestPropsUsingArray = {
path: requestPathUsingArray,
method: RequestMethod.COPY,
};
it('should enhance class with expected request metadata', () => {
class Test {
@Copy(requestPath)
public static test() {}
@Copy(requestPathUsingArray)
public static testUsingArray() {}
}
const path = Reflect.getMetadata('path', Test.test);
const method = Reflect.getMetadata('method', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
expect(path).to.be.eql(requestPath);
expect(method).to.be.eql(requestProps.method);
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
});
it('should set path on "/" by default', () => {
class Test {
@Copy()
public static test(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
@Copy([])
public static testUsingArray(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
}
const path = Reflect.getMetadata('path', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
expect(path).to.be.eql('/');
expect(pathUsingArray).to.be.eql('/');
});
});
describe('@Move', () => {
const requestPath = 'test';
const requestProps = {
path: requestPath,
method: RequestMethod.MOVE,
};
const requestPathUsingArray = ['foo', 'bar'];
const requestPropsUsingArray = {
path: requestPathUsingArray,
method: RequestMethod.MOVE,
};
it('should enhance class with expected request metadata', () => {
class Test {
@Move(requestPath)
public static test() {}
@Move(requestPathUsingArray)
public static testUsingArray() {}
}
const path = Reflect.getMetadata('path', Test.test);
const method = Reflect.getMetadata('method', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
expect(path).to.be.eql(requestPath);
expect(method).to.be.eql(requestProps.method);
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
});
it('should set path on "/" by default', () => {
class Test {
@Move()
public static test(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
@Move([])
public static testUsingArray(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
}
const path = Reflect.getMetadata('path', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
expect(path).to.be.eql('/');
expect(pathUsingArray).to.be.eql('/');
});
});
describe('@Lock', () => {
const requestPath = 'test';
const requestProps = {
path: requestPath,
method: RequestMethod.LOCK,
};
const requestPathUsingArray = ['foo', 'bar'];
const requestPropsUsingArray = {
path: requestPathUsingArray,
method: RequestMethod.LOCK,
};
it('should enhance class with expected request metadata', () => {
class Test {
@Lock(requestPath)
public static test() {}
@Lock(requestPathUsingArray)
public static testUsingArray() {}
}
const path = Reflect.getMetadata('path', Test.test);
const method = Reflect.getMetadata('method', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
expect(path).to.be.eql(requestPath);
expect(method).to.be.eql(requestProps.method);
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
});
it('should set path on "/" by default', () => {
class Test {
@Lock()
public static test(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
@Lock([])
public static testUsingArray(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
}
const path = Reflect.getMetadata('path', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
expect(path).to.be.eql('/');
expect(pathUsingArray).to.be.eql('/');
});
});
describe('@Unlock', () => {
const requestPath = 'test';
const requestProps = {
path: requestPath,
method: RequestMethod.UNLOCK,
};
const requestPathUsingArray = ['foo', 'bar'];
const requestPropsUsingArray = {
path: requestPathUsingArray,
method: RequestMethod.UNLOCK,
};
it('should enhance class with expected request metadata', () => {
class Test {
@Unlock(requestPath)
public static test() {}
@Unlock(requestPathUsingArray)
public static testUsingArray() {}
}
const path = Reflect.getMetadata('path', Test.test);
const method = Reflect.getMetadata('method', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
expect(path).to.be.eql(requestPath);
expect(method).to.be.eql(requestProps.method);
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
});
it('should set path on "/" by default', () => {
class Test {
@Unlock()
public static test(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
@Unlock([])
public static testUsingArray(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
}
const path = Reflect.getMetadata('path', Test.test);
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
expect(path).to.be.eql('/');
expect(pathUsingArray).to.be.eql('/');
});
});

View File

@@ -1,71 +0,0 @@
import { expect } from 'chai';
import { BadRequestException } from '../../exceptions';
import { ParseDatePipe } from '../../pipes/parse-date.pipe';
describe('ParseDatePipe', () => {
let target: ParseDatePipe;
beforeEach(() => {
target = new ParseDatePipe();
});
describe('transform', () => {
describe('when validation passes', () => {
it('should return a valid date object', () => {
const date = new Date().toISOString();
const transformedDate = target.transform(date);
expect(transformedDate).to.be.instanceOf(Date);
expect(transformedDate.toISOString()).to.equal(date);
const asNumber = transformedDate.getTime();
const transformedNumber = target.transform(asNumber);
expect(transformedNumber).to.be.instanceOf(Date);
expect(transformedNumber.getTime()).to.equal(asNumber);
});
it('should not throw an error if the value is undefined/null and optional is true', () => {
const target = new ParseDatePipe({ optional: true });
const value = target.transform(undefined);
expect(value).to.equal(undefined);
});
});
describe('when default value is provided', () => {
it('should return the default value if the value is undefined/null', () => {
const defaultValue = new Date();
const target = new ParseDatePipe({
optional: true,
default: () => defaultValue,
});
const value = target.transform(undefined);
expect(value).to.equal(defaultValue);
});
});
describe('when validation fails', () => {
it('should throw an error', () => {
try {
target.transform('123abc');
expect.fail();
} catch (error) {
expect(error).to.be.instanceOf(BadRequestException);
expect(error.message).to.equal(
'Validation failed (invalid date format)',
);
}
});
});
describe('when empty value', () => {
it('should throw an error', () => {
try {
target.transform('');
expect.fail();
} catch (error) {
expect(error).to.be.instanceOf(BadRequestException);
expect(error.message).to.equal(
'Validation failed (no Date provided)',
);
}
});
});
});
});

View File

@@ -205,18 +205,6 @@ describe('ValidationPipe', () => {
}),
).to.be.equal(+value);
});
it('should parse undefined to undefined', async () => {
target = new ValidationPipe({ transform: true });
const value = undefined;
expect(
await target.transform(value, {
metatype: Number,
data: 'test',
type: 'query',
}),
).to.be.undefined;
});
});
describe('when input is a path parameter (number)', () => {
it('should parse to number', async () => {
@@ -231,18 +219,6 @@ describe('ValidationPipe', () => {
}),
).to.be.equal(+value);
});
it('should parse undefined to undefined', async () => {
target = new ValidationPipe({ transform: true });
const value = undefined;
expect(
await target.transform(value, {
metatype: Number,
data: 'test',
type: 'param',
}),
).to.be.undefined;
});
});
describe('when input is a query parameter (boolean)', () => {
it('should parse the string "true" to the boolean true', async () => {

View File

@@ -125,12 +125,9 @@ describe('Logger', () => {
Logger.error(error);
expect(processStderrWriteSpy.calledOnce).to.be.true;
expect(processStderrWriteSpy.firstCall.firstArg).to.include(`Object:`);
expect(processStderrWriteSpy.firstCall.firstArg).to.include(
`Object(${Object.keys(error).length})`,
);
expect(processStderrWriteSpy.firstCall.firstArg).to.include(
`randomError: \x1b[33mtrue`,
`{\n "randomError": true\n}`,
);
});
@@ -184,153 +181,6 @@ describe('Logger', () => {
expect(processStderrWriteSpy.thirdCall.firstArg).to.equal(stack + '\n');
});
});
describe('when the default logger is used and json mode is enabled', () => {
const logger = new ConsoleLogger({ json: true });
let processStdoutWriteSpy: sinon.SinonSpy;
let processStderrWriteSpy: sinon.SinonSpy;
beforeEach(() => {
processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
processStderrWriteSpy = sinon.spy(process.stderr, 'write');
});
afterEach(() => {
processStdoutWriteSpy.restore();
processStderrWriteSpy.restore();
});
it('should print error with stack as JSON to the console', () => {
const errorMessage = 'error message';
const error = new Error(errorMessage);
logger.error(error.message, error.stack);
const json = JSON.parse(processStderrWriteSpy.firstCall?.firstArg);
expect(json.pid).to.equal(process.pid);
expect(json.level).to.equal('error');
expect(json.message).to.equal(errorMessage);
});
it('should log out to stdout as JSON', () => {
const message = 'message 1';
logger.log(message);
const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);
expect(json.pid).to.equal(process.pid);
expect(json.level).to.equal('log');
expect(json.message).to.equal(message);
});
it('should log out an error to stderr as JSON', () => {
const message = 'message 1';
logger.error(message);
const json = JSON.parse(processStderrWriteSpy.firstCall?.firstArg);
expect(json.pid).to.equal(process.pid);
expect(json.level).to.equal('error');
expect(json.message).to.equal(message);
});
it('should log Map object', () => {
const map = new Map([
['key1', 'value1'],
['key2', 'value2'],
]);
logger.log(map);
const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);
expect(json.pid).to.equal(process.pid);
expect(json.level).to.equal('log');
expect(json.message).to.equal(
`Map(2) { 'key1' => 'value1', 'key2' => 'value2' }`,
);
});
it('should log Set object', () => {
const set = new Set(['value1', 'value2']);
logger.log(set);
const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);
expect(json.pid).to.equal(process.pid);
expect(json.level).to.equal('log');
expect(json.message).to.equal(`Set(2) { 'value1', 'value2' }`);
});
it('should log bigint', () => {
const bigInt = BigInt(9007199254740991);
logger.log(bigInt);
const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);
expect(json.pid).to.equal(process.pid);
expect(json.level).to.equal('log');
expect(json.message).to.equal('9007199254740991');
});
it('should log symbol', () => {
const symbol = Symbol('test');
logger.log(symbol);
const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);
expect(json.pid).to.equal(process.pid);
expect(json.level).to.equal('log');
expect(json.message).to.equal('Symbol(test)');
});
});
describe('when the default logger is used, json mode is enabled and compact is false (utils.inspect)', () => {
const logger = new ConsoleLogger({ json: true, compact: false });
let processStdoutWriteSpy: sinon.SinonSpy;
let processStderrWriteSpy: sinon.SinonSpy;
beforeEach(() => {
processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
processStderrWriteSpy = sinon.spy(process.stderr, 'write');
});
afterEach(() => {
processStdoutWriteSpy.restore();
processStderrWriteSpy.restore();
});
it('should log out to stdout as JSON (utils.inspect)', () => {
const message = 'message 1';
logger.log(message);
const json = convertInspectToJSON(
processStdoutWriteSpy.firstCall?.firstArg,
);
expect(json.pid).to.equal(process.pid);
expect(json.level).to.equal('log');
expect(json.message).to.equal(message);
});
it('should log out an error to stderr as JSON (utils.inspect)', () => {
const message = 'message 1';
logger.error(message);
const json = convertInspectToJSON(
processStderrWriteSpy.firstCall?.firstArg,
);
expect(json.pid).to.equal(process.pid);
expect(json.level).to.equal('error');
expect(json.message).to.equal(message);
});
});
describe('when logging is disabled', () => {
let processStdoutWriteSpy: sinon.SinonSpy;
let previousLoggerRef: LoggerService;
@@ -718,7 +568,6 @@ describe('Logger', () => {
expect(processStdoutWriteSpy.called).to.be.false;
});
});
describe('when custom logger is being used', () => {
class CustomLogger implements LoggerService {
log(message: any, context?: string) {}
@@ -874,7 +723,7 @@ describe('Logger', () => {
}
}
const consoleLogger = new CustomConsoleLogger({ colors: false });
const consoleLogger = new CustomConsoleLogger();
const consoleLoggerSpy = sinon.spy(
consoleLogger,
'stringifyMessage' as keyof ConsoleLogger,
@@ -890,40 +739,30 @@ describe('Logger', () => {
expect(consoleLoggerSpy.getCall(0).returnValue).to.equal('str1');
expect(consoleLoggerSpy.getCall(1).returnValue).to.equal(
`Object(1) {
key: 'str2'
}`,
`Object:
{
"key": "str2"
}
`,
);
expect(consoleLoggerSpy.getCall(2).returnValue).to.equal(
`Array(1) [
'str3'
]`,
`Object:
[
"str3"
]
`,
);
expect(consoleLoggerSpy.getCall(3).returnValue).to.equal(
`Array(1) [
`Object:
[
{
key: 'str4'
"key": "str4"
}
]`,
]
`,
);
expect(consoleLoggerSpy.getCall(4).returnValue).to.equal('null');
expect(consoleLoggerSpy.getCall(5).returnValue).to.equal('1');
expect(consoleLoggerSpy.getCall(4).returnValue).to.equal(null);
expect(consoleLoggerSpy.getCall(5).returnValue).to.equal(1);
});
});
});
function convertInspectToJSON(inspectOutput: string) {
const jsonLikeString = inspectOutput
.replace(/'([^']+)'/g, '"$1"') // single-quoted strings
.replace(/([a-zA-Z0-9_]+):/g, '"$1":') // unquoted object keys
.replace(/\bundefined\b/g, 'null')
.replace(/\[Function(: [^\]]+)?\]/g, '"[Function]"')
.replace(/\[Circular\]/g, '"[Circular]"');
try {
return JSON.parse(jsonLikeString);
} catch (error) {
console.error('Error parsing the modified inspect output:', error);
throw error;
}
}

View File

@@ -94,7 +94,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td><a href="https://www.mercedes-benz.com/" target="_blank"><img src="https://nestjs.com/img/logos/mercedes-logo.png" width="100" valign="middle" /></a></td>
<td><a href="https://www.dinii.jp/" target="_blank"><img src="https://nestjs.com/img/logos/dinii-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://bloodycase.com/?promocode=NEST" target="_blank"><img src="https://nestjs.com/img/logos/bloodycase-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-logo.svg" width="150" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-dark-logo.svg#2" width="150" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
</tr>

View File

@@ -62,48 +62,6 @@ export abstract class AbstractHttpAdapter<
return this.instance.patch(...args);
}
public propfind(handler: RequestHandler);
public propfind(path: any, handler: RequestHandler);
public propfind(...args: any[]) {
return this.instance.propfind(...args);
}
public proppatch(handler: RequestHandler);
public proppatch(path: any, handler: RequestHandler);
public proppatch(...args: any[]) {
return this.instance.proppatch(...args);
}
public mkcol(handler: RequestHandler);
public mkcol(path: any, handler: RequestHandler);
public mkcol(...args: any[]) {
return this.instance.mkcol(...args);
}
public copy(handler: RequestHandler);
public copy(path: any, handler: RequestHandler);
public copy(...args: any[]) {
return this.instance.copy(...args);
}
public move(handler: RequestHandler);
public move(path: any, handler: RequestHandler);
public move(...args: any[]) {
return this.instance.move(...args);
}
public lock(handler: RequestHandler);
public lock(path: any, handler: RequestHandler);
public lock(...args: any[]) {
return this.instance.lock(...args);
}
public unlock(handler: RequestHandler);
public unlock(path: any, handler: RequestHandler);
public unlock(...args: any[]) {
return this.instance.unlock(...args);
}
public all(handler: RequestHandler);
public all(path: any, handler: RequestHandler);
public all(...args: any[]) {

View File

@@ -1,9 +1,14 @@
import { RuntimeException } from './exceptions/runtime.exception';
import { Logger } from '@nestjs/common/services/logger.service';
export class ExceptionHandler {
private static readonly logger = new Logger(ExceptionHandler.name);
public handle(exception: Error) {
ExceptionHandler.logger.error(exception);
public handle(exception: RuntimeException | Error) {
if (!(exception instanceof RuntimeException)) {
ExceptionHandler.logger.error(exception.message, exception.stack);
return;
}
ExceptionHandler.logger.error(exception.what(), exception.stack);
}
}

View File

@@ -68,6 +68,12 @@ export class BaseExceptionFilter<T = any> implements ExceptionFilter<T> {
applicationRef.end(response);
}
if (this.isExceptionObject(exception)) {
return BaseExceptionFilter.logger.error(
exception.message,
exception.stack,
);
}
return BaseExceptionFilter.logger.error(exception);
}

View File

@@ -5,7 +5,7 @@ export class ExternalExceptionFilter<T = any, R = any> {
catch(exception: T, host: ArgumentsHost): R | Promise<R> {
if (exception instanceof Error && !(exception instanceof HttpException)) {
ExternalExceptionFilter.logger.error(exception);
ExternalExceptionFilter.logger.error(exception.message, exception.stack);
}
throw exception;
}

View File

@@ -1,3 +1,4 @@
import { Observable, Subject } from 'rxjs';
import { AbstractHttpAdapter } from '../adapters/http-adapter';
/**
@@ -16,6 +17,8 @@ export class HttpAdapterHost<
T extends AbstractHttpAdapter = AbstractHttpAdapter,
> {
private _httpAdapter?: T;
private _listen$ = new Subject<void>();
private isListening = false;
/**
* Accessor for the underlying `HttpAdapter`
@@ -35,4 +38,31 @@ export class HttpAdapterHost<
get httpAdapter(): T {
return this._httpAdapter;
}
/**
* Observable that allows to subscribe to the `listen` event.
* This event is emitted when the HTTP application is listening for incoming requests.
*/
get listen$(): Observable<void> {
return this._listen$.asObservable();
}
/**
* Sets the listening state of the application.
*/
set listening(listening: boolean) {
this.isListening = listening;
if (listening) {
this._listen$.next();
this._listen$.complete();
}
}
/**
* Returns a boolean indicating whether the application is listening for incoming requests.
*/
get listening(): boolean {
return this.isListening;
}
}

View File

@@ -11,13 +11,6 @@ const REQUEST_METHOD_MAP = {
[RequestMethod.OPTIONS]: 'options',
[RequestMethod.HEAD]: 'head',
[RequestMethod.SEARCH]: 'search',
[RequestMethod.PROPFIND]: 'propfind',
[RequestMethod.PROPPATCH]: 'proppatch',
[RequestMethod.MKCOL]: 'mkcol',
[RequestMethod.COPY]: 'copy',
[RequestMethod.MOVE]: 'move',
[RequestMethod.LOCK]: 'lock',
[RequestMethod.UNLOCK]: 'unlock',
} as const satisfies Record<RequestMethod, keyof HttpServer>;
export class RouterMethodFactory {

View File

@@ -3,7 +3,7 @@ import {
ForwardReference,
Type,
} from '@nestjs/common/interfaces';
import { ModuleOpaqueKeyFactory } from './opaque-key-factory/interfaces/module-opaque-key-factory.interface';
import { ModuleTokenFactory } from './module-token-factory';
export interface ModuleFactory {
type: Type<any>;
@@ -12,59 +12,36 @@ export interface ModuleFactory {
}
export class ModuleCompiler {
constructor(
private readonly _moduleOpaqueKeyFactory: ModuleOpaqueKeyFactory,
) {}
get moduleOpaqueKeyFactory(): ModuleOpaqueKeyFactory {
return this._moduleOpaqueKeyFactory;
}
constructor(private readonly moduleTokenFactory = new ModuleTokenFactory()) {}
public async compile(
moduleClsOrDynamic:
| Type
| DynamicModule
| ForwardReference
| Promise<DynamicModule>,
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
): Promise<ModuleFactory> {
moduleClsOrDynamic = await moduleClsOrDynamic;
const { type, dynamicMetadata } = this.extractMetadata(moduleClsOrDynamic);
const token = dynamicMetadata
? this._moduleOpaqueKeyFactory.createForDynamic(
type,
dynamicMetadata,
moduleClsOrDynamic as DynamicModule | ForwardReference,
)
: this._moduleOpaqueKeyFactory.createForStatic(
type,
moduleClsOrDynamic as Type,
);
const { type, dynamicMetadata } = this.extractMetadata(await metatype);
const token = this.moduleTokenFactory.create(type, dynamicMetadata);
return { type, dynamicMetadata, token };
}
public extractMetadata(
moduleClsOrDynamic: Type | ForwardReference | DynamicModule,
metatype: Type<any> | ForwardReference | DynamicModule,
): {
type: Type;
dynamicMetadata: Omit<DynamicModule, 'module'> | undefined;
type: Type<any>;
dynamicMetadata?: Partial<DynamicModule> | undefined;
} {
if (!this.isDynamicModule(moduleClsOrDynamic)) {
if (!this.isDynamicModule(metatype)) {
return {
type: (moduleClsOrDynamic as ForwardReference)?.forwardRef
? (moduleClsOrDynamic as ForwardReference).forwardRef()
: moduleClsOrDynamic,
dynamicMetadata: undefined,
type: (metatype as ForwardReference)?.forwardRef
? (metatype as ForwardReference).forwardRef()
: metatype,
};
}
const { module: type, ...dynamicMetadata } = moduleClsOrDynamic;
const { module: type, ...dynamicMetadata } = metatype;
return { type, dynamicMetadata };
}
public isDynamicModule(
moduleClsOrDynamic: Type | DynamicModule | ForwardReference,
): moduleClsOrDynamic is DynamicModule {
return !!(moduleClsOrDynamic as DynamicModule).module;
module: Type<any> | DynamicModule | ForwardReference,
): module is DynamicModule {
return !!(module as DynamicModule).module;
}
}

View File

@@ -4,7 +4,6 @@ import {
GLOBAL_MODULE_METADATA,
} from '@nestjs/common/constants';
import { Injectable, Type } from '@nestjs/common/interfaces';
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
import { ApplicationConfig } from '../application-config';
import { DiscoverableMetaHostCollection } from '../discovery/discoverable-meta-host-collection';
import {
@@ -20,16 +19,16 @@ import { ContextId } from './instance-wrapper';
import { InternalCoreModule } from './internal-core-module/internal-core-module';
import { InternalProvidersStorage } from './internal-providers-storage';
import { Module } from './module';
import { ModuleTokenFactory } from './module-token-factory';
import { ModulesContainer } from './modules-container';
import { ByReferenceModuleOpaqueKeyFactory } from './opaque-key-factory/by-reference-module-opaque-key-factory';
import { DeepHashedModuleOpaqueKeyFactory } from './opaque-key-factory/deep-hashed-module-opaque-key-factory';
import { ModuleOpaqueKeyFactory } from './opaque-key-factory/interfaces/module-opaque-key-factory.interface';
type ModuleMetatype = Type<any> | DynamicModule | Promise<DynamicModule>;
type ModuleScope = Type<any>[];
export class NestContainer {
private readonly globalModules = new Set<Module>();
private readonly moduleTokenFactory = new ModuleTokenFactory();
private readonly moduleCompiler = new ModuleCompiler(this.moduleTokenFactory);
private readonly modules = new ModulesContainer();
private readonly dynamicModulesMetadata = new Map<
string,
@@ -37,27 +36,11 @@ export class NestContainer {
>();
private readonly internalProvidersStorage = new InternalProvidersStorage();
private readonly _serializedGraph = new SerializedGraph();
private moduleCompiler: ModuleCompiler;
private internalCoreModule: Module;
constructor(
private readonly _applicationConfig:
| ApplicationConfig
| undefined = undefined,
private readonly _contextOptions:
| NestApplicationContextOptions
| undefined = undefined,
) {
const moduleOpaqueKeyFactory =
this._contextOptions?.moduleIdGeneratorAlgorithm === 'deep-hash'
? new DeepHashedModuleOpaqueKeyFactory()
: new ByReferenceModuleOpaqueKeyFactory({
keyGenerationStrategy: this._contextOptions?.snapshot
? 'shallow'
: 'random',
});
this.moduleCompiler = new ModuleCompiler(moduleOpaqueKeyFactory);
}
private readonly _applicationConfig: ApplicationConfig = undefined,
) {}
get serializedGraph(): SerializedGraph {
return this._serializedGraph;
@@ -338,8 +321,8 @@ export class NestContainer {
this.modules[InternalCoreModule.name] = moduleRef;
}
public getModuleTokenFactory(): ModuleOpaqueKeyFactory {
return this.moduleCompiler.moduleOpaqueKeyFactory;
public getModuleTokenFactory(): ModuleTokenFactory {
return this.moduleTokenFactory;
}
public registerRequestProvider<T = any>(request: T, contextId: ContextId) {

View File

@@ -170,6 +170,11 @@ export class Injector {
inquirer,
);
} catch (err) {
wrapper.removeInstanceByContextId(
this.getContextId(contextId, wrapper),
inquirerId,
);
settlementSignal.error(err);
throw err;
}

View File

@@ -168,6 +168,21 @@ export class InstanceWrapper<T = any> {
collection.set(contextId, value);
}
public removeInstanceByContextId(contextId: ContextId, inquirerId?: string) {
if (this.scope === Scope.TRANSIENT && inquirerId) {
return this.removeInstanceByInquirerId(contextId, inquirerId);
}
this.values.delete(contextId);
}
public removeInstanceByInquirerId(contextId: ContextId, inquirerId: string) {
const collection = this.transientMap.get(inquirerId);
if (!collection) {
return;
}
collection.delete(contextId);
}
public addCtorMetadata(index: number, wrapper: InstanceWrapper) {
if (!this[INSTANCE_METADATA_SYMBOL].dependencies) {
this[INSTANCE_METADATA_SYMBOL].dependencies = [];

View File

@@ -1,48 +1,34 @@
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
import { DynamicModule, Logger } from '@nestjs/common';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { Logger } from '@nestjs/common/services/logger.service';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { isFunction, isSymbol } from '@nestjs/common/utils/shared.utils';
import { createHash } from 'crypto';
import stringify from 'fast-safe-stringify';
import { ModuleOpaqueKeyFactory } from './interfaces/module-opaque-key-factory.interface';
import { performance } from 'perf_hooks';
const CLASS_STR = 'class ';
const CLASS_STR_LEN = CLASS_STR.length;
export class DeepHashedModuleOpaqueKeyFactory
implements ModuleOpaqueKeyFactory
{
private readonly moduleIdsCache = new WeakMap<Type<unknown>, string>();
export class ModuleTokenFactory {
private readonly moduleTokenCache = new Map<string, string>();
private readonly logger = new Logger(DeepHashedModuleOpaqueKeyFactory.name, {
private readonly moduleIdsCache = new WeakMap<Type<unknown>, string>();
private readonly logger = new Logger(ModuleTokenFactory.name, {
timestamp: true,
});
public createForStatic(moduleCls: Type): string {
const moduleId = this.getModuleId(moduleCls);
const moduleName = this.getModuleName(moduleCls);
const key = `${moduleId}_${moduleName}`;
if (this.moduleTokenCache.has(key)) {
return this.moduleTokenCache.get(key);
}
const hash = this.hashString(key);
this.moduleTokenCache.set(key, hash);
return hash;
}
public createForDynamic(
moduleCls: Type<unknown>,
dynamicMetadata: Omit<DynamicModule, 'module'>,
public create(
metatype: Type<unknown>,
dynamicModuleMetadata?: Partial<DynamicModule> | undefined,
): string {
const moduleId = this.getModuleId(moduleCls);
const moduleName = this.getModuleName(moduleCls);
const moduleId = this.getModuleId(metatype);
if (!dynamicModuleMetadata) {
return this.getStaticModuleToken(moduleId, this.getModuleName(metatype));
}
const opaqueToken = {
id: moduleId,
module: moduleName,
dynamic: dynamicMetadata,
module: this.getModuleName(metatype),
dynamic: dynamicModuleMetadata,
};
const start = performance.now();
const opaqueTokenString = this.getStringifiedOpaqueToken(opaqueToken);
@@ -51,13 +37,24 @@ export class DeepHashedModuleOpaqueKeyFactory
if (timeSpentInMs > 10) {
const formattedTimeSpent = timeSpentInMs.toFixed(2);
this.logger.warn(
`The module "${opaqueToken.module}" is taking ${formattedTimeSpent}ms to serialize, this may be caused by larger objects statically assigned to the module. Consider changing the "moduleIdGeneratorAlgorithm" option to "reference" to improve the performance.`,
`The module "${opaqueToken.module}" is taking ${formattedTimeSpent}ms to serialize, this may be caused by larger objects statically assigned to the module. More details: https://github.com/nestjs/nest/issues/12738`,
);
}
return this.hashString(opaqueTokenString);
}
public getStaticModuleToken(moduleId: string, moduleName: string): string {
const key = `${moduleId}_${moduleName}`;
if (this.moduleTokenCache.has(key)) {
return this.moduleTokenCache.get(key);
}
const hash = this.hashString(key);
this.moduleTokenCache.set(key, hash);
return hash;
}
public getStringifiedOpaqueToken(opaqueToken: object | undefined): string {
// Uses safeStringify instead of JSON.stringify to support circular dynamic modules
// The replacer function is also required in order to obtain real class names

View File

@@ -653,12 +653,7 @@ export class Module {
private generateUuid(): string {
const prefix = 'M_';
const key = this.token
? this.token.includes(':')
? this.token.split(':')[1]
: this.token
: this.name;
const key = this.name?.toString() ?? this.token?.toString();
return key ? UuidFactory.get(`${prefix}_${key}`) : randomStringGenerator();
}
}

View File

@@ -1,63 +0,0 @@
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { createHash } from 'crypto';
import { ModuleOpaqueKeyFactory } from './interfaces/module-opaque-key-factory.interface';
const K_MODULE_ID = Symbol('K_MODULE_ID');
export class ByReferenceModuleOpaqueKeyFactory
implements ModuleOpaqueKeyFactory
{
private readonly keyGenerationStrategy: 'random' | 'shallow';
constructor(options?: { keyGenerationStrategy: 'random' | 'shallow' }) {
this.keyGenerationStrategy = options?.keyGenerationStrategy ?? 'random';
}
public createForStatic(
moduleCls: Type,
originalRef: Type | ForwardReference = moduleCls,
): string {
return this.getOrCreateModuleId(moduleCls, undefined, originalRef);
}
public createForDynamic(
moduleCls: Type<unknown>,
dynamicMetadata: Omit<DynamicModule, 'module'>,
originalRef: DynamicModule | ForwardReference,
): string {
return this.getOrCreateModuleId(moduleCls, dynamicMetadata, originalRef);
}
private getOrCreateModuleId(
moduleCls: Type<unknown>,
dynamicMetadata: Partial<DynamicModule> | undefined,
originalRef: Type | DynamicModule | ForwardReference,
): string {
if (originalRef[K_MODULE_ID]) {
return originalRef[K_MODULE_ID];
}
let moduleId: string;
if (this.keyGenerationStrategy === 'random') {
moduleId = this.generateRandomString();
} else {
moduleId = dynamicMetadata
? `${this.generateRandomString()}:${this.hashString(moduleCls.name + JSON.stringify(dynamicMetadata))}`
: `${this.generateRandomString()}:${this.hashString(moduleCls.toString())}`;
}
originalRef[K_MODULE_ID] = moduleId;
return moduleId;
}
private hashString(value: string): string {
return createHash('sha256').update(value).digest('hex');
}
private generateRandomString(): string {
return randomStringGenerator();
}
}

View File

@@ -1,26 +0,0 @@
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
import { Type } from '@nestjs/common/interfaces/type.interface';
export interface ModuleOpaqueKeyFactory {
/**
* Creates a unique opaque key for the given static module.
* @param moduleCls A static module class.
* @param originalRef Original object reference. In most cases, it's the same as `moduleCls`.
*/
createForStatic(
moduleCls: Type,
originalRef: Type | ForwardReference,
): string;
/**
* Creates a unique opaque key for the given dynamic module.
* @param moduleCls A dynamic module class reference.
* @param dynamicMetadata Dynamic module metadata.
* @param originalRef Original object reference.
*/
createForDynamic(
moduleCls: Type<unknown>,
dynamicMetadata: Omit<DynamicModule, 'module'>,
originalRef: DynamicModule | ForwardReference,
): string;
}

View File

@@ -9,7 +9,6 @@ import {
Abstract,
DynamicModule,
GetOrResolveOptions,
SelectOptions,
Type,
} from '@nestjs/common/interfaces';
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
@@ -51,10 +50,11 @@ export class NestApplicationContext<
private shouldFlushLogsOnOverride = false;
private readonly activeShutdownSignals = new Array<string>();
private readonly moduleCompiler: ModuleCompiler;
private readonly moduleCompiler = new ModuleCompiler();
private shutdownCleanupRef?: (...args: unknown[]) => unknown;
private _instanceLinksHost: InstanceLinksHost;
private _moduleRefsForHooksByDistance?: Array<Module>;
private initializationPromise?: Promise<void>;
protected get instanceLinksHost() {
if (!this._instanceLinksHost) {
@@ -71,7 +71,6 @@ export class NestApplicationContext<
) {
super();
this.injector = new Injector();
this.moduleCompiler = container.getModuleCompiler();
if (this.appOptions.preview) {
this.printInPreviewModeWarning();
@@ -89,7 +88,6 @@ export class NestApplicationContext<
*/
public select<T>(
moduleType: Type<T> | DynamicModule,
selectOptions?: SelectOptions,
): INestApplicationContext {
const modulesContainer = this.container.getModules();
const contextModuleCtor = this.contextModule.metatype;
@@ -98,30 +96,15 @@ export class NestApplicationContext<
const moduleTokenFactory = this.container.getModuleTokenFactory();
const { type, dynamicMetadata } =
this.moduleCompiler.extractMetadata(moduleType);
const token = dynamicMetadata
? moduleTokenFactory.createForDynamic(
type,
dynamicMetadata,
moduleType as DynamicModule,
)
: moduleTokenFactory.createForStatic(type, moduleType as Type);
const token = moduleTokenFactory.create(type, dynamicMetadata);
const selectedModule = modulesContainer.get(token);
if (!selectedModule) {
throw new UnknownModuleException(type.name);
}
const options =
typeof selectOptions?.abortOnError !== 'undefined'
? {
...this.appOptions,
...selectOptions,
}
: this.appOptions;
return new NestApplicationContext(
this.container,
options,
this.appOptions,
selectedModule,
scope,
);
@@ -252,8 +235,16 @@ export class NestApplicationContext<
if (this.isInitialized) {
return this;
}
await this.callInitHook();
await this.callBootstrapHook();
this.initializationPromise = new Promise(async (resolve, reject) => {
try {
await this.callInitHook();
await this.callBootstrapHook();
resolve();
} catch (err) {
reject(err);
}
});
await this.initializationPromise;
this.isInitialized = true;
return this;
@@ -264,6 +255,7 @@ export class NestApplicationContext<
* @returns {Promise<void>}
*/
public async close(signal?: string): Promise<void> {
await this.initializationPromise;
await this.callDestroyHook();
await this.callBeforeShutdownHook(signal);
await this.dispose();
@@ -351,6 +343,7 @@ export class NestApplicationContext<
return;
}
receivedSignal = true;
await this.initializationPromise;
await this.callDestroyHook();
await this.callBeforeShutdownHook(signal);
await this.dispose();
@@ -402,10 +395,7 @@ export class NestApplicationContext<
* modules and its children.
*/
protected async callDestroyHook(): Promise<void> {
const modulesSortedByDistance = [
...this.getModulesToTriggerHooksOn(),
].reverse();
const modulesSortedByDistance = this.getModulesToTriggerHooksOn();
for (const module of modulesSortedByDistance) {
await callModuleDestroyHook(module);
}

View File

@@ -294,8 +294,12 @@ export class NestApplication
public async listen(port: number | string, hostname: string): Promise<any>;
public async listen(port: number | string, ...args: any[]): Promise<any> {
this.assertNotInPreviewMode('listen');
!this.isInitialized && (await this.init());
if (!this.isInitialized) {
await this.init();
}
const httpAdapterHost = this.container.getHttpAdapterHostRef();
return new Promise((resolve, reject) => {
const errorHandler = (e: any) => {
this.logger.error(e?.toString?.());
@@ -323,6 +327,8 @@ export class NestApplication
if (address) {
this.httpServer.removeListener('error', errorHandler);
this.isListening = true;
httpAdapterHost.listening = true;
resolve(this.httpServer);
}
if (isCallbackInOriginalArgs) {

View File

@@ -3,9 +3,6 @@ import {
INestApplication,
INestApplicationContext,
INestMicroservice,
DynamicModule,
ForwardReference,
Type,
} from '@nestjs/common';
import { NestMicroserviceOptions } from '@nestjs/common/interfaces/microservices/nest-microservice-options.interface';
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
@@ -30,12 +27,6 @@ import { NestApplication } from './nest-application';
import { NestApplicationContext } from './nest-application-context';
import { DependenciesScanner } from './scanner';
type IEntryNestModule =
| Type<any>
| DynamicModule
| ForwardReference
| Promise<IEntryNestModule>;
/**
* @publicApi
*/
@@ -56,7 +47,7 @@ export class NestFactoryStatic {
* contains a reference to the NestApplication instance.
*/
public async create<T extends INestApplication = INestApplication>(
module: IEntryNestModule,
module: any,
options?: NestApplicationOptions,
): Promise<T>;
/**
@@ -71,12 +62,12 @@ export class NestFactoryStatic {
* contains a reference to the NestApplication instance.
*/
public async create<T extends INestApplication = INestApplication>(
module: IEntryNestModule,
module: any,
httpAdapter: AbstractHttpAdapter,
options?: NestApplicationOptions,
): Promise<T>;
public async create<T extends INestApplication = INestApplication>(
moduleCls: IEntryNestModule,
moduleCls: any,
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
options?: NestApplicationOptions,
): Promise<T> {
@@ -121,7 +112,7 @@ export class NestFactoryStatic {
* contains a reference to the NestMicroservice instance.
*/
public async createMicroservice<T extends object>(
moduleCls: IEntryNestModule,
moduleCls: any,
options?: NestMicroserviceOptions & T,
): Promise<INestMicroservice> {
const { NestMicroservice } = loadPackage(
@@ -163,7 +154,7 @@ export class NestFactoryStatic {
* contains a reference to the NestApplicationContext instance.
*/
public async createApplicationContext(
moduleCls: IEntryNestModule,
moduleCls: any,
options?: NestApplicationContextOptions,
): Promise<INestApplicationContext> {
const applicationConfig = new ApplicationConfig();

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/core",
"version": "10.4.7",
"version": "10.4.8",
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -36,7 +36,7 @@
"uid": "2.0.2"
},
"devDependencies": {
"@nestjs/common": "10.4.7"
"@nestjs/common": "10.4.8"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0",

View File

@@ -92,10 +92,10 @@ export class DependenciesScanner {
overrides: options?.overrides,
});
await this.scanModulesForDependencies();
this.calculateModulesDistance();
this.addScopedEnhancersMetadata();
this.container.bindGlobalScope();
this.calculateModulesDistance();
}
public async scanForModules({
@@ -389,16 +389,12 @@ export class DependenciesScanner {
// Skip "InternalCoreModule" from calculating distance
modulesGenerator.next();
const calculateDistance = (
moduleRef: Module,
distance = 1,
modulesStack = [],
) => {
const localModulesStack = [...modulesStack];
if (!moduleRef || localModulesStack.includes(moduleRef)) {
const modulesStack = [];
const calculateDistance = (moduleRef: Module, distance = 1) => {
if (!moduleRef || modulesStack.includes(moduleRef)) {
return;
}
localModulesStack.push(moduleRef);
modulesStack.push(moduleRef);
const moduleImports = moduleRef.imports;
moduleImports.forEach(importedModuleRef => {
@@ -406,7 +402,7 @@ export class DependenciesScanner {
if (distance > importedModuleRef.distance) {
importedModuleRef.distance = distance;
}
calculateDistance(importedModuleRef, distance + 1, localModulesStack);
calculateDistance(importedModuleRef, distance + 1);
}
});
};

View File

@@ -127,10 +127,14 @@ export class Reflector {
* @param targets context (decorated objects) to retrieve metadata from
*
*/
public getAll<TParam = any, TTransformed = TParam>(
decorator: ReflectableDecorator<TParam, TTransformed>,
public getAll<T extends ReflectableDecorator<any>>(
decorator: T,
targets: (Type<any> | Function)[],
): TTransformed extends Array<any> ? TTransformed : TTransformed[];
): T extends ReflectableDecorator<infer R>
? R extends Array<any>
? R
: R[]
: unknown;
/**
* Retrieve metadata for a specified key for a specified set of targets.
*
@@ -165,14 +169,10 @@ export class Reflector {
* @param targets context (decorated objects) to retrieve metadata from
*
*/
public getAllAndMerge<TParam = any, TTransformed = TParam>(
decorator: ReflectableDecorator<TParam, TTransformed>,
public getAllAndMerge<T extends ReflectableDecorator<any>>(
decorator: T,
targets: (Type<any> | Function)[],
): TTransformed extends Array<any>
? TTransformed
: TTransformed extends object
? TTransformed
: TTransformed[];
): T extends ReflectableDecorator<infer R> ? R : unknown;
/**
* Retrieve metadata for a specified key for a specified set of targets and merge results.
*
@@ -203,13 +203,6 @@ export class Reflector {
if (isEmpty(metadataCollection)) {
return metadataCollection as TResult;
}
if (metadataCollection.length === 1) {
const value = metadataCollection[0];
if (isObject(value)) {
return value as TResult;
}
return metadataCollection as TResult;
}
return metadataCollection.reduce((a, b) => {
if (Array.isArray(a)) {
return a.concat(b);
@@ -231,10 +224,10 @@ export class Reflector {
* @param targets context (decorated objects) to retrieve metadata from
*
*/
public getAllAndOverride<TParam = any, TTransformed = TParam>(
decorator: ReflectableDecorator<TParam, TTransformed>,
public getAllAndOverride<T extends ReflectableDecorator<any>>(
decorator: T,
targets: (Type<any> | Function)[],
): TTransformed;
): T extends ReflectableDecorator<infer R> ? R : unknown;
/**
* Retrieve metadata for a specified key for a specified set of targets and return a first not undefined value.
*

View File

@@ -1,7 +1,8 @@
import { expect } from 'chai';
import * as sinon from 'sinon';
import { expect } from 'chai';
import { ExceptionHandler } from '../../../errors/exception-handler';
import { RuntimeException } from '../../../errors/exceptions/runtime.exception';
import { InvalidMiddlewareException } from '../../../errors/exceptions/invalid-middleware.exception';
describe('ExceptionHandler', () => {
let instance: ExceptionHandler;
@@ -9,7 +10,7 @@ describe('ExceptionHandler', () => {
instance = new ExceptionHandler();
});
describe('handle', () => {
let logger: { error: Function };
let logger;
let errorSpy: sinon.SinonSpy;
beforeEach(() => {
logger = {
@@ -18,10 +19,16 @@ describe('ExceptionHandler', () => {
(ExceptionHandler as any).logger = logger;
errorSpy = sinon.spy(logger, 'error');
});
it('should call the logger.error method with the thrown exception passed as an argument', () => {
it('when exception is instanceof RuntimeException', () => {
const exception = new RuntimeException('msg');
instance.handle(exception);
expect(errorSpy.calledWith(exception)).to.be.true;
expect(errorSpy.calledWith(exception.message, exception.stack)).to.be
.true;
});
it('when exception is not instanceof RuntimeException', () => {
const exception = new InvalidMiddlewareException('msg');
instance.handle(exception);
expect(errorSpy.calledWith(exception.what(), exception.stack)).to.be.true;
});
});
});

View File

@@ -2,11 +2,27 @@ import { expect } from 'chai';
import { HttpAdapterHost } from '../../helpers/http-adapter-host';
describe('HttpAdapterHost', () => {
const applicationRefHost = new HttpAdapterHost();
let applicationRefHost: HttpAdapterHost;
beforeEach(() => {
applicationRefHost = new HttpAdapterHost();
});
it('should wrap application reference', () => {
const ref = {};
applicationRefHost.httpAdapter = ref as any;
expect(applicationRefHost.httpAdapter).to.be.eql(ref);
});
it('should emit listen event when listening is set to true', done => {
applicationRefHost.listen$.subscribe(() => {
expect(applicationRefHost.listening).to.be.true;
done();
});
applicationRefHost.listening = true;
});
it('listening should return false if the application isnt listening yet', () => {
expect(applicationRefHost.listening).to.be.false;
});
});

View File

@@ -14,13 +14,6 @@ describe('RouterMethodFactory', () => {
patch: () => {},
options: () => {},
head: () => {},
propfind: () => {},
proppatch: () => {},
mkcol: () => {},
copy: () => {},
move: () => {},
lock: () => {},
unlock: () => {},
all: () => {},
};
beforeEach(() => {
@@ -36,17 +29,6 @@ describe('RouterMethodFactory', () => {
expect(factory.get(target, RequestMethod.PATCH)).to.equal(target.patch);
expect(factory.get(target, RequestMethod.OPTIONS)).to.equal(target.options);
expect(factory.get(target, RequestMethod.HEAD)).to.equal(target.head);
expect(factory.get(target, RequestMethod.PROPFIND)).to.equal(
target.propfind,
);
expect(factory.get(target, RequestMethod.PROPPATCH)).to.equal(
target.proppatch,
);
expect(factory.get(target, RequestMethod.MKCOL)).to.equal(target.mkcol);
expect(factory.get(target, RequestMethod.COPY)).to.equal(target.copy);
expect(factory.get(target, RequestMethod.MOVE)).to.equal(target.move);
expect(factory.get(target, RequestMethod.LOCK)).to.equal(target.lock);
expect(factory.get(target, RequestMethod.UNLOCK)).to.equal(target.unlock);
expect(factory.get(target, -1 as any)).to.equal(target.use);
});
});

View File

@@ -1,30 +1,28 @@
import { expect } from 'chai';
import { ModuleCompiler } from '../../injector/compiler';
import { ByReferenceModuleOpaqueKeyFactory } from '../../injector/opaque-key-factory/by-reference-module-opaque-key-factory';
describe('ModuleCompiler', () => {
let compiler: ModuleCompiler;
beforeEach(() => {
compiler = new ModuleCompiler(new ByReferenceModuleOpaqueKeyFactory());
compiler = new ModuleCompiler();
});
describe('extractMetadata', () => {
describe('when module is a dynamic module', () => {
it('should return object with "type" and "dynamicMetadata" property', () => {
it('should return object with "type" and "dynamicMetadata" property', async () => {
const obj = { module: 'test', providers: [] };
const { module, ...dynamicMetadata } = obj;
expect(compiler.extractMetadata(obj as any)).to.be.deep.equal({
expect(await compiler.extractMetadata(obj as any)).to.be.deep.equal({
type: module,
dynamicMetadata,
});
});
});
describe('when module is a not dynamic module', () => {
it('should return object with "type" property', () => {
it('should return object with "type" property', async () => {
const type = 'test';
expect(compiler.extractMetadata(type as any)).to.be.deep.equal({
expect(await compiler.extractMetadata(type as any)).to.be.deep.equal({
type,
dynamicMetadata: undefined,
});
});
});

View File

@@ -9,7 +9,6 @@ import { NoopHttpAdapter } from '../utils/noop-adapter.spec';
describe('NestContainer', () => {
let container: NestContainer;
let untypedContainer: any;
@Module({})
class TestModule {}
@@ -20,7 +19,6 @@ describe('NestContainer', () => {
beforeEach(() => {
container = new NestContainer();
untypedContainer = container as any;
});
it('should "addProvider" throw "UnknownModuleException" when module is not stored in collection', () => {
@@ -55,7 +53,7 @@ describe('NestContainer', () => {
describe('clear', () => {
it('should call `clear` on modules collection', () => {
const clearSpy = sinon.spy(untypedContainer.modules, 'clear');
const clearSpy = sinon.spy((container as any).modules, 'clear');
container.clear();
expect(clearSpy.called).to.be.true;
});
@@ -65,7 +63,7 @@ describe('NestContainer', () => {
it('should not add module if already exists in collection', async () => {
const modules = new Map();
const setSpy = sinon.spy(modules, 'set');
untypedContainer.modules = modules;
(container as any).modules = modules;
await container.addModule(TestModule as any, []);
await container.addModule(TestModule as any, []);
@@ -91,7 +89,7 @@ describe('NestContainer', () => {
const modules = new Map();
const setSpy = sinon.spy(modules, 'set');
untypedContainer.modules = modules;
(container as any).modules = modules;
await container.addModule(TestModule as any, []);
await container.replaceModule(
@@ -176,7 +174,7 @@ describe('NestContainer', () => {
beforeEach(() => {
token = 'token';
collection = new Map();
untypedContainer.dynamicModulesMetadata = collection;
(container as any).dynamicModulesMetadata = collection;
});
describe('when dynamic metadata exists', () => {
it('should add to the dynamic metadata collection', () => {
@@ -217,7 +215,7 @@ describe('NestContainer', () => {
describe('get applicationConfig', () => {
it('should return ApplicationConfig instance', () => {
expect(container.applicationConfig).to.be.eql(
untypedContainer._applicationConfig,
(container as any)._applicationConfig,
);
});
});
@@ -227,7 +225,7 @@ describe('NestContainer', () => {
const httpAdapter = new NoopHttpAdapter({});
container.setHttpAdapter(httpAdapter);
const internalStorage = untypedContainer.internalProvidersStorage;
const internalStorage = (container as any).internalProvidersStorage;
expect(internalStorage.httpAdapter).to.be.eql(httpAdapter);
});
});
@@ -246,7 +244,7 @@ describe('NestContainer', () => {
it('should register core module ref', () => {
const ref = {} as any;
container.registerCoreModuleRef(ref);
expect(untypedContainer.internalCoreModule).to.be.eql(ref);
expect((container as any).internalCoreModule).to.be.eql(ref);
});
});
});

View File

@@ -1,6 +1,7 @@
import { Scope } from '@nestjs/common';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { createContextId } from '../../helpers';
import { STATIC_CONTEXT } from '../../injector/constants';
import { InstanceWrapper } from '../../injector/instance-wrapper';
@@ -737,6 +738,53 @@ describe('InstanceWrapper', () => {
});
});
describe('removeInstanceByContextId', () => {
describe('without inquirer', () => {
it('should remove instance for given context', () => {
const wrapper = new InstanceWrapper({
scope: Scope.TRANSIENT,
});
const contextId = createContextId();
wrapper.setInstanceByContextId(contextId, { instance: {} });
const existingContext = wrapper.getInstanceByContextId(contextId);
expect(existingContext.instance).to.be.not.undefined;
wrapper.removeInstanceByContextId(contextId);
const removedContext = wrapper.getInstanceByContextId(contextId);
expect(removedContext.instance).to.be.undefined;
});
});
describe('when transient and inquirer has been passed', () => {
it('should remove instance for given context', () => {
const wrapper = new InstanceWrapper({
scope: Scope.TRANSIENT,
});
wrapper.setInstanceByContextId(
STATIC_CONTEXT,
{ instance: {} },
'inquirerId',
);
const existingContext = wrapper.getInstanceByContextId(
STATIC_CONTEXT,
'inquirerId',
);
expect(existingContext.instance).to.be.not.undefined;
wrapper.removeInstanceByContextId(STATIC_CONTEXT, 'inquirerId');
const removedContext = wrapper.getInstanceByContextId(
STATIC_CONTEXT,
'inquirerId',
);
expect(removedContext.instance).to.be.undefined;
});
});
});
describe('isInRequestScope', () => {
describe('when tree and context are not static and is not transient', () => {
it('should return true', () => {

View File

@@ -1,48 +1,41 @@
import { expect } from 'chai';
import * as sinon from 'sinon';
import { DeepHashedModuleOpaqueKeyFactory } from '../../../injector/opaque-key-factory/deep-hashed-module-opaque-key-factory';
import { ModuleTokenFactory } from '../../injector/module-token-factory';
describe('DeepHashedModuleOpaqueKeyFactory', () => {
describe('ModuleTokenFactory', () => {
const moduleId = 'constId';
let factory: DeepHashedModuleOpaqueKeyFactory;
let factory: ModuleTokenFactory;
beforeEach(() => {
factory = new DeepHashedModuleOpaqueKeyFactory();
factory = new ModuleTokenFactory();
sinon.stub(factory, 'getModuleId').returns(moduleId);
});
describe('createForStatic', () => {
describe('create', () => {
class Module {}
it('should return expected token', () => {
const type = Module;
const token1 = factory.createForStatic(type);
const token2 = factory.createForStatic(type);
const token1 = factory.create(type, undefined);
const token2 = factory.create(type, undefined);
expect(token1).to.be.deep.eq(token2);
});
});
describe('createForDynamic', () => {
class Module {}
it('should include dynamic metadata', () => {
const type = Module;
const token1 = factory.createForDynamic(type, {
const token1 = factory.create(type, {
providers: [{}],
} as any);
const token2 = factory.createForDynamic(type, {
const token2 = factory.create(type, {
providers: [{}],
} as any);
expect(token1).to.be.deep.eq(token2);
});
});
describe('getModuleName', () => {
it('should map module metatype to name', () => {
const metatype = () => {};
expect(factory.getModuleName(metatype as any)).to.be.eql(metatype.name);
});
});
describe('getStringifiedOpaqueToken', () => {
describe('when metadata exists', () => {
it('should return hash', () => {
@@ -87,7 +80,6 @@ describe('DeepHashedModuleOpaqueKeyFactory', () => {
);
});
});
describe('when metadata does not exist', () => {
it('should return empty string', () => {
expect(factory.getStringifiedOpaqueToken(undefined)).to.be.eql('');

View File

@@ -11,8 +11,7 @@ import { InstanceWrapper } from '../../injector/instance-wrapper';
import { Module } from '../../injector/module';
describe('Module', () => {
let moduleRef: Module;
let untypedModuleRef: any;
let module: Module;
let container: NestContainer;
@ModuleDecorator({})
@@ -23,24 +22,23 @@ describe('Module', () => {
beforeEach(() => {
container = new NestContainer();
moduleRef = new Module(TestModule, container);
untypedModuleRef = moduleRef as any;
module = new Module(TestModule, container);
});
it('should add controller', () => {
const collection = new Map();
const setSpy = sinon.spy(collection, 'set');
untypedModuleRef._controllers = collection;
(module as any)._controllers = collection;
@Controller({ scope: Scope.REQUEST, durable: true })
class Test {}
moduleRef.addController(Test);
module.addController(Test);
expect(
setSpy.calledWith(
Test,
new InstanceWrapper({
host: moduleRef,
host: module,
token: Test,
name: 'Test',
scope: Scope.REQUEST,
@@ -56,14 +54,14 @@ describe('Module', () => {
it('should add injectable', () => {
const collection = new Map();
const setSpy = sinon.spy(collection, 'set');
untypedModuleRef._injectables = collection;
(module as any)._injectables = collection;
moduleRef.addInjectable(TestProvider, 'interceptor', TestModule);
module.addInjectable(TestProvider, 'interceptor', TestModule);
expect(
setSpy.calledWith(
TestProvider,
new InstanceWrapper({
host: moduleRef,
host: module,
name: 'TestProvider',
token: TestProvider,
scope: undefined,
@@ -79,9 +77,9 @@ describe('Module', () => {
describe('when injectable is custom provided', () => {
it('should call `addCustomProvider`', () => {
const addCustomProviderSpy = sinon.spy(moduleRef, 'addCustomProvider');
const addCustomProviderSpy = sinon.spy(module, 'addCustomProvider');
moduleRef.addInjectable({ provide: 'test' } as any, 'guard');
module.addInjectable({ provide: 'test' } as any, 'guard');
expect(addCustomProviderSpy.called).to.be.true;
});
});
@@ -89,14 +87,14 @@ describe('Module', () => {
it('should add provider', () => {
const collection = new Map();
const setSpy = sinon.spy(collection, 'set');
untypedModuleRef._providers = collection;
(module as any)._providers = collection;
moduleRef.addProvider(TestProvider);
module.addProvider(TestProvider);
expect(
setSpy.calledWith(
TestProvider,
new InstanceWrapper({
host: moduleRef,
host: module,
name: 'TestProvider',
token: TestProvider,
scope: undefined,
@@ -111,81 +109,81 @@ describe('Module', () => {
it('should call "addCustomProvider" when "provide" property exists', () => {
const addCustomProvider = sinon.spy();
moduleRef.addCustomProvider = addCustomProvider;
module.addCustomProvider = addCustomProvider;
const provider = { provide: 'test', useValue: 'test' };
moduleRef.addProvider(provider as any);
module.addProvider(provider as any);
expect((addCustomProvider as sinon.SinonSpy).called).to.be.true;
});
it('should call "addCustomClass" when "useClass" property exists', () => {
const addCustomClass = sinon.spy();
moduleRef.addCustomClass = addCustomClass;
module.addCustomClass = addCustomClass;
const provider = { provide: 'test', useClass: () => null };
moduleRef.addCustomProvider(provider as any, new Map());
module.addCustomProvider(provider as any, new Map());
expect((addCustomClass as sinon.SinonSpy).called).to.be.true;
});
it('should call "addCustomValue" when "useValue" property exists', () => {
const addCustomValue = sinon.spy();
moduleRef.addCustomValue = addCustomValue;
module.addCustomValue = addCustomValue;
const provider = { provide: 'test', useValue: () => null };
moduleRef.addCustomProvider(provider as any, new Map());
module.addCustomProvider(provider as any, new Map());
expect((addCustomValue as sinon.SinonSpy).called).to.be.true;
});
it('should call "addCustomValue" when "useValue" property exists but its value is `undefined`', () => {
const addCustomValue = sinon.spy();
moduleRef.addCustomValue = addCustomValue;
module.addCustomValue = addCustomValue;
const provider = { provide: 'test', useValue: undefined };
moduleRef.addCustomProvider(provider as any, new Map());
module.addCustomProvider(provider as any, new Map());
expect((addCustomValue as sinon.SinonSpy).called).to.be.true;
});
it('should call "addCustomFactory" when "useFactory" property exists', () => {
const addCustomFactory = sinon.spy();
moduleRef.addCustomFactory = addCustomFactory;
module.addCustomFactory = addCustomFactory;
const provider = { provide: 'test', useFactory: () => null };
moduleRef.addCustomProvider(provider as any, new Map());
module.addCustomProvider(provider as any, new Map());
expect((addCustomFactory as sinon.SinonSpy).called).to.be.true;
});
it('should call "addCustomUseExisting" when "useExisting" property exists', () => {
const addCustomUseExisting = sinon.spy();
moduleRef.addCustomUseExisting = addCustomUseExisting;
module.addCustomUseExisting = addCustomUseExisting;
const provider = { provide: 'test', useExisting: () => null };
moduleRef.addCustomUseExisting(provider as any, new Map());
module.addCustomUseExisting(provider as any, new Map());
expect((addCustomUseExisting as sinon.SinonSpy).called).to.be.true;
});
describe('addCustomClass', () => {
const type = { name: 'TypeTest' };
const provider = { provide: type, useClass: type, durable: true };
let setSpy: sinon.SinonSpy;
let setSpy;
beforeEach(() => {
const collection = new Map();
setSpy = sinon.spy(collection, 'set');
untypedModuleRef._providers = collection;
(module as any)._providers = collection;
});
it('should store provider', () => {
moduleRef.addCustomClass(provider as any, untypedModuleRef._providers);
module.addCustomClass(provider as any, (module as any)._providers);
expect(
setSpy.calledWith(
provider.provide,
new InstanceWrapper({
host: moduleRef,
host: module,
token: type as any,
name: provider.provide.name,
scope: undefined,
@@ -201,23 +199,23 @@ describe('Module', () => {
});
describe('addCustomValue', () => {
let setSpy: sinon.SinonSpy;
let setSpy;
const value = () => ({});
const provider = { provide: value, useValue: value };
beforeEach(() => {
const collection = new Map();
setSpy = sinon.spy(collection, 'set');
untypedModuleRef._providers = collection;
(module as any)._providers = collection;
});
it('should store provider', () => {
moduleRef.addCustomValue(provider as any, untypedModuleRef._providers);
module.addCustomValue(provider as any, (module as any)._providers);
expect(
setSpy.calledWith(
provider.provide,
new InstanceWrapper({
host: moduleRef,
host: module,
token: provider.provide,
name: provider.provide.name,
scope: Scope.DEFAULT,
@@ -237,20 +235,20 @@ describe('Module', () => {
const inject = [1, 2, 3];
const provider = { provide: type, useFactory: type, inject, durable: true };
let setSpy: sinon.SinonSpy;
let setSpy;
beforeEach(() => {
const collection = new Map();
setSpy = sinon.spy(collection, 'set');
untypedModuleRef._providers = collection;
(module as any)._providers = collection;
});
it('should store provider', () => {
moduleRef.addCustomFactory(provider as any, untypedModuleRef._providers);
module.addCustomFactory(provider as any, (module as any)._providers);
expect(
setSpy.calledWith(
provider.provide,
new InstanceWrapper({
host: moduleRef,
host: module,
token: provider.provide as any,
name: provider.provide.name,
scope: undefined,
@@ -270,18 +268,15 @@ describe('Module', () => {
const type = { name: 'TypeTest' };
const provider = { provide: type, useExisting: type };
let setSpy: sinon.SinonSpy;
let setSpy;
beforeEach(() => {
const collection = new Map();
setSpy = sinon.spy(collection, 'set');
untypedModuleRef._providers = collection;
(module as any)._providers = collection;
});
it('should store provider', () => {
moduleRef.addCustomUseExisting(
provider as any,
untypedModuleRef._providers,
);
const factoryFn = untypedModuleRef._providers.get(
module.addCustomUseExisting(provider as any, (module as any)._providers);
const factoryFn = (module as any)._providers.get(
provider.provide,
).metatype;
@@ -290,7 +285,7 @@ describe('Module', () => {
setSpy.calledWith(
token,
new InstanceWrapper({
host: moduleRef,
host: module,
token,
name: provider.provide.name,
metatype: factoryFn,
@@ -309,41 +304,41 @@ describe('Module', () => {
describe('when get instance', () => {
describe('when metatype does not exists in providers collection', () => {
beforeEach(() => {
sinon.stub(untypedModuleRef._providers, 'has').returns(false);
sinon.stub((module as any)._providers, 'has').returns(false);
});
it('should throw RuntimeException', () => {
expect(() => moduleRef.instance).to.throws(RuntimeException);
expect(() => module.instance).to.throws(RuntimeException);
});
});
describe('when metatype exists in providers collection', () => {
it('should return null', () => {
expect(moduleRef.instance).to.be.eql(null);
expect(module.instance).to.be.eql(null);
});
});
});
describe('when exported provider is custom provided', () => {
beforeEach(() => {
sinon.stub(moduleRef, 'validateExportedProvider').callsFake(o => o);
sinon.stub(module, 'validateExportedProvider').callsFake(o => o);
});
it('should call `addCustomExportedProvider`', () => {
const addCustomExportedProviderSpy = sinon.spy(
moduleRef,
module,
'addCustomExportedProvider',
);
moduleRef.addExportedProvider({ provide: 'test' } as any);
module.addExportedProvider({ provide: 'test' } as any);
expect(addCustomExportedProviderSpy.called).to.be.true;
});
it('should support symbols', () => {
const addCustomExportedProviderSpy = sinon.spy(
moduleRef,
module,
'addCustomExportedProvider',
);
const symb = Symbol('test');
moduleRef.addExportedProvider({ provide: symb } as any);
module.addExportedProvider({ provide: symb } as any);
expect(addCustomExportedProviderSpy.called).to.be.true;
expect(untypedModuleRef._exports.has(symb)).to.be.true;
expect((module as any)._exports.has(symb)).to.be.true;
});
});
@@ -353,10 +348,10 @@ describe('Module', () => {
const wrapper = {
mergeWith: sinon.spy(),
};
sinon.stub(moduleRef, 'hasProvider').callsFake(() => true);
sinon.stub(moduleRef.providers, 'get').callsFake(() => wrapper as any);
sinon.stub(module, 'hasProvider').callsFake(() => true);
sinon.stub(module.providers, 'get').callsFake(() => wrapper as any);
moduleRef.replace(null, { isProvider: true });
module.replace(null, { isProvider: true });
expect(wrapper.mergeWith.called).to.be.true;
});
});
@@ -366,12 +361,10 @@ describe('Module', () => {
mergeWith: sinon.spy(),
isProvider: true,
};
sinon.stub(moduleRef, 'hasInjectable').callsFake(() => true);
sinon
.stub(moduleRef.injectables, 'get')
.callsFake(() => wrapper as any);
sinon.stub(module, 'hasInjectable').callsFake(() => true);
sinon.stub(module.injectables, 'get').callsFake(() => wrapper as any);
moduleRef.replace(null, {});
module.replace(null, {});
expect(wrapper.mergeWith.called).to.be.true;
});
});
@@ -380,63 +373,61 @@ describe('Module', () => {
describe('imports', () => {
it('should return relatedModules', () => {
const test = ['test'];
untypedModuleRef._imports = test;
(module as any)._imports = test;
expect(moduleRef.imports).to.be.eql(test);
expect(module.imports).to.be.eql(test);
});
});
describe('injectables', () => {
it('should return injectables', () => {
const test = ['test'];
untypedModuleRef._injectables = test;
expect(moduleRef.injectables).to.be.eql(test);
(module as any)._injectables = test;
expect(module.injectables).to.be.eql(test);
});
});
describe('controllers', () => {
it('should return controllers', () => {
const test = ['test'];
untypedModuleRef._controllers = test;
(module as any)._controllers = test;
expect(moduleRef.controllers).to.be.eql(test);
expect(module.controllers).to.be.eql(test);
});
});
describe('exports', () => {
it('should return exports', () => {
const test = ['test'];
untypedModuleRef._exports = test;
(module as any)._exports = test;
expect(moduleRef.exports).to.be.eql(test);
expect(module.exports).to.be.eql(test);
});
});
describe('providers', () => {
it('should return providers', () => {
const test = ['test'];
untypedModuleRef._providers = test;
(module as any)._providers = test;
expect(moduleRef.providers).to.be.eql(test);
expect(module.providers).to.be.eql(test);
});
});
describe('createModuleReferenceType', () => {
let customModuleRef: any;
let moduleRef: any;
beforeEach(() => {
const Class = moduleRef.createModuleReferenceType();
customModuleRef = new Class();
const Class = module.createModuleReferenceType();
moduleRef = new Class();
});
it('should return metatype with "get" method', () => {
expect(!!customModuleRef.get).to.be.true;
expect(!!moduleRef.get).to.be.true;
});
describe('get', () => {
it('should throw exception if not exists', () => {
expect(() => customModuleRef.get('fail')).to.throws(
UnknownElementException,
);
expect(() => moduleRef.get('fail')).to.throws(UnknownElementException);
});
});
});
@@ -445,22 +436,22 @@ describe('Module', () => {
describe('when unit exists in provider collection', () => {
it('should behave as identity', () => {
untypedModuleRef._providers = new Map([[token, true]]);
expect(moduleRef.validateExportedProvider(token)).to.be.eql(token);
(module as any)._providers = new Map([[token, true]]);
expect(module.validateExportedProvider(token)).to.be.eql(token);
});
});
describe('when unit exists in related modules collection', () => {
it('should behave as identity', () => {
class Random {}
untypedModuleRef._imports = new Set([
(module as any)._imports = new Set([
new Module(Random, new NestContainer()),
]);
expect(moduleRef.validateExportedProvider(Random)).to.be.eql(Random);
expect(module.validateExportedProvider(Random)).to.be.eql(Random);
});
});
describe('when unit does not exist in both provider and related modules collections', () => {
it('should throw UnknownExportException', () => {
expect(() => moduleRef.validateExportedProvider(token)).to.throws(
expect(() => module.validateExportedProvider(token)).to.throws(
UnknownExportException,
);
});
@@ -471,13 +462,13 @@ describe('Module', () => {
describe('when module has provider', () => {
it('should return true', () => {
const token = 'test';
moduleRef.providers.set(token, new InstanceWrapper());
expect(moduleRef.hasProvider(token)).to.be.true;
module.providers.set(token, new InstanceWrapper());
expect(module.hasProvider(token)).to.be.true;
});
});
describe('otherwise', () => {
it('should return false', () => {
expect(moduleRef.hasProvider('_')).to.be.false;
expect(module.hasProvider('_')).to.be.false;
});
});
});
@@ -486,33 +477,33 @@ describe('Module', () => {
describe('when module has injectable', () => {
it('should return true', () => {
const token = 'test';
moduleRef.injectables.set(token, new InstanceWrapper());
expect(moduleRef.hasInjectable(token)).to.be.true;
module.injectables.set(token, new InstanceWrapper());
expect(module.hasInjectable(token)).to.be.true;
});
});
describe('otherwise', () => {
it('should return false', () => {
expect(moduleRef.hasInjectable('_')).to.be.false;
expect(module.hasInjectable('_')).to.be.false;
});
});
});
describe('getter "id"', () => {
it('should return module id', () => {
expect(moduleRef.id).to.be.equal(moduleRef['_id']);
expect(module.id).to.be.equal(module['_id']);
});
});
describe('getProviderByKey', () => {
describe('when does not exist', () => {
it('should return undefined', () => {
expect(moduleRef.getProviderByKey('test')).to.be.undefined;
expect(module.getProviderByKey('test')).to.be.undefined;
});
});
describe('otherwise', () => {
it('should return instance wrapper', () => {
moduleRef.addProvider(TestProvider);
expect(moduleRef.getProviderByKey(TestProvider)).to.not.be.undefined;
module.addProvider(TestProvider);
expect(module.getProviderByKey(TestProvider)).to.not.be.undefined;
});
});
});

View File

@@ -1,111 +0,0 @@
import { expect } from 'chai';
import * as sinon from 'sinon';
import { ByReferenceModuleOpaqueKeyFactory } from '../../../injector/opaque-key-factory/by-reference-module-opaque-key-factory';
describe('ByReferenceModuleOpaqueKeyFactory', () => {
const moduleId = 'constId';
let factory: ByReferenceModuleOpaqueKeyFactory;
describe('when generating algorithm is random', () => {
beforeEach(() => {
factory = new ByReferenceModuleOpaqueKeyFactory();
sinon.stub(factory as any, 'generateRandomString').returns(moduleId);
});
describe('createForStatic', () => {
class Module {}
it('should return expected token', () => {
const type = Module;
const token1 = factory.createForStatic(type);
const token2 = factory.createForStatic(type);
expect(token1).to.be.deep.eq(token2);
});
});
describe('createForDynamic', () => {
class Module {}
it('should include dynamic metadata', () => {
const dynamicModule = {
module: Module,
providers: [
{
provide: 'test',
useValue: 'test',
},
],
};
const token1 = factory.createForDynamic(
dynamicModule.module,
{
providers: dynamicModule.providers,
},
dynamicModule,
);
const token2 = factory.createForDynamic(
dynamicModule.module,
{
providers: dynamicModule.providers,
},
dynamicModule,
);
expect(token1).to.be.deep.eq(token2);
});
});
});
describe('when generating algorithm is shallow', () => {
beforeEach(() => {
factory = new ByReferenceModuleOpaqueKeyFactory({
keyGenerationStrategy: 'shallow',
});
sinon.stub(factory as any, 'generateRandomString').returns(moduleId);
});
describe('createForStatic', () => {
class Module {}
it('should return expected token', () => {
const type = Module;
const token1 = factory.createForStatic(type);
const token2 = factory.createForStatic(type);
expect(token1).to.be.deep.eq(token2);
});
});
describe('createForDynamic', () => {
class Module {}
it('should include dynamic metadata', () => {
const dynamicModule = {
module: Module,
providers: [
{
provide: 'test',
useValue: 'test',
},
],
};
const token1 = factory.createForDynamic(
dynamicModule.module,
{
providers: dynamicModule.providers,
},
dynamicModule,
);
const token2 = factory.createForDynamic(
dynamicModule.module,
{
providers: dynamicModule.providers,
},
dynamicModule,
);
expect(token1).to.be.deep.eq(token2);
});
});
});
});

View File

@@ -1,4 +1,4 @@
import { InjectionToken, Scope } from '@nestjs/common';
import { InjectionToken, Provider, Scope } from '@nestjs/common';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { ContextIdFactory } from '../helpers/context-id-factory';
@@ -7,6 +7,7 @@ import { Injector } from '../injector/injector';
import { InstanceLoader } from '../injector/instance-loader';
import { GraphInspector } from '../inspector/graph-inspector';
import { NestApplicationContext } from '../nest-application-context';
import { setTimeout } from 'timers/promises';
describe('NestApplicationContext', () => {
class A {}
@@ -14,6 +15,7 @@ describe('NestApplicationContext', () => {
async function testHelper(
injectionKey: InjectionToken,
scope: Scope,
additionalProviders: Array<Provider> = [],
): Promise<NestApplicationContext> {
const nestContainer = new NestContainer();
const injector = new Injector();
@@ -33,6 +35,10 @@ describe('NestApplicationContext', () => {
moduleRef.token,
);
for (const provider of additionalProviders) {
nestContainer.addProvider(provider, moduleRef.token);
}
nestContainer.addInjectable(
{
provide: injectionKey,
@@ -96,6 +102,58 @@ describe('NestApplicationContext', () => {
expect(processUp).to.be.false;
expect(promisesResolved).to.be.true;
});
it('should defer shutdown until all init hooks are resolved', async () => {
const clock = sinon.useFakeTimers({
toFake: ['setTimeout'],
});
const signal = 'SIGTERM';
const onModuleInitStub = sinon.stub();
const onApplicationShutdownStub = sinon.stub();
class B {
async onModuleInit() {
await setTimeout(5000);
onModuleInitStub();
}
async onApplicationShutdown() {
await setTimeout(1000);
onApplicationShutdownStub();
}
}
const applicationContext = await testHelper(A, Scope.DEFAULT, [
{ provide: B, useClass: B, scope: Scope.DEFAULT },
]);
applicationContext.enableShutdownHooks([signal]);
const ignoreProcessSignal = () => {
// noop to prevent process from exiting
};
process.on(signal, ignoreProcessSignal);
const deferredShutdown = async () => {
setTimeout(1);
process.kill(process.pid, signal);
};
Promise.all([applicationContext.init(), deferredShutdown()]);
await clock.nextAsync();
expect(onModuleInitStub.called).to.be.false;
expect(onApplicationShutdownStub.called).to.be.false;
await clock.nextAsync();
expect(onModuleInitStub.called).to.be.true;
expect(onApplicationShutdownStub.called).to.be.false;
await clock.nextAsync();
expect(onModuleInitStub.called).to.be.true;
expect(onApplicationShutdownStub.called).to.be.true;
clock.restore();
});
});
describe('get', () => {

View File

@@ -4,13 +4,9 @@ import { RouteParamsFactory } from '../../router/route-params-factory';
describe('RouteParamsFactory', () => {
let factory: RouteParamsFactory;
let untypedFactory: any;
beforeEach(() => {
factory = new RouteParamsFactory();
untypedFactory = factory as any;
});
describe('exchangeKeyForValue', () => {
const res = {};
const next = () => ({});
@@ -41,14 +37,14 @@ describe('RouteParamsFactory', () => {
describe(`RouteParamtypes.NEXT`, () => {
it('should return next object', () => {
expect(
untypedFactory.exchangeKeyForValue(RouteParamtypes.NEXT, ...args),
(factory as any).exchangeKeyForValue(RouteParamtypes.NEXT, ...args),
).to.be.eql(next);
});
});
describe(`RouteParamtypes.RESPONSE`, () => {
it('should return response object', () => {
expect(
untypedFactory.exchangeKeyForValue(
(factory as any).exchangeKeyForValue(
RouteParamtypes.RESPONSE,
...args,
),
@@ -58,7 +54,7 @@ describe('RouteParamsFactory', () => {
describe(`RouteParamtypes.REQUEST`, () => {
it('should return request object', () => {
expect(
untypedFactory.exchangeKeyForValue(
(factory as any).exchangeKeyForValue(
RouteParamtypes.REQUEST,
...args,
),
@@ -68,14 +64,14 @@ describe('RouteParamsFactory', () => {
describe(`RouteParamtypes.BODY`, () => {
it('should return body object', () => {
expect(
untypedFactory.exchangeKeyForValue(RouteParamtypes.BODY, ...args),
(factory as any).exchangeKeyForValue(RouteParamtypes.BODY, ...args),
).to.be.eql(req.body);
});
});
describe(`RouteParamtypes.RAW_BODY`, () => {
it('should return rawBody buffer', () => {
expect(
untypedFactory.exchangeKeyForValue(
(factory as any).exchangeKeyForValue(
RouteParamtypes.RAW_BODY,
...args,
),
@@ -85,7 +81,7 @@ describe('RouteParamsFactory', () => {
describe(`RouteParamtypes.HEADERS`, () => {
it('should return headers object', () => {
expect(
untypedFactory.exchangeKeyForValue(
(factory as any).exchangeKeyForValue(
RouteParamtypes.HEADERS,
...args,
),
@@ -95,14 +91,14 @@ describe('RouteParamsFactory', () => {
describe(`RouteParamtypes.IP`, () => {
it('should return ip property', () => {
expect(
untypedFactory.exchangeKeyForValue(RouteParamtypes.IP, ...args),
(factory as any).exchangeKeyForValue(RouteParamtypes.IP, ...args),
).to.be.equal(req.ip);
});
});
describe(`RouteParamtypes.SESSION`, () => {
it('should return session object', () => {
expect(
untypedFactory.exchangeKeyForValue(
(factory as any).exchangeKeyForValue(
RouteParamtypes.SESSION,
...args,
),
@@ -112,41 +108,50 @@ describe('RouteParamsFactory', () => {
describe(`RouteParamtypes.QUERY`, () => {
it('should return query object', () => {
expect(
untypedFactory.exchangeKeyForValue(RouteParamtypes.QUERY, ...args),
(factory as any).exchangeKeyForValue(
RouteParamtypes.QUERY,
...args,
),
).to.be.eql(req.query);
});
});
describe(`RouteParamtypes.PARAM`, () => {
it('should return params object', () => {
expect(
untypedFactory.exchangeKeyForValue(RouteParamtypes.PARAM, ...args),
(factory as any).exchangeKeyForValue(
RouteParamtypes.PARAM,
...args,
),
).to.be.eql(req.params);
});
});
describe(`RouteParamtypes.HOST`, () => {
it('should return hosts object', () => {
expect(
untypedFactory.exchangeKeyForValue(RouteParamtypes.HOST, ...args),
(factory as any).exchangeKeyForValue(RouteParamtypes.HOST, ...args),
).to.be.eql(req.hosts);
});
});
describe(`RouteParamtypes.FILE`, () => {
it('should return file object', () => {
expect(
untypedFactory.exchangeKeyForValue(RouteParamtypes.FILE, ...args),
(factory as any).exchangeKeyForValue(RouteParamtypes.FILE, ...args),
).to.be.eql(req.file);
});
});
describe(`RouteParamtypes.FILES`, () => {
it('should return files object', () => {
expect(
untypedFactory.exchangeKeyForValue(RouteParamtypes.FILES, ...args),
(factory as any).exchangeKeyForValue(
RouteParamtypes.FILES,
...args,
),
).to.be.eql(req.files);
});
});
describe('not available', () => {
it('should return null', () => {
expect(untypedFactory.exchangeKeyForValue(-1, ...args)).to.be.eql(
expect((factory as any).exchangeKeyForValue(-1, ...args)).to.be.eql(
null,
);
});

View File

@@ -52,7 +52,6 @@ describe('RoutesResolver', () => {
let router: any;
let routesResolver: RoutesResolver;
let untypedRoutesResolver: any;
let container: NestContainer;
let modules: Map<string, any>;
let applicationRef: any;
@@ -83,7 +82,6 @@ describe('RoutesResolver', () => {
new Injector(),
new GraphInspector(container),
);
untypedRoutesResolver = routesResolver as any;
});
describe('registerRouters', () => {
@@ -97,14 +95,14 @@ describe('RoutesResolver', () => {
const appInstance = new NoopHttpAdapter(router);
const exploreSpy = sinon.spy(
untypedRoutesResolver.routerExplorer,
(routesResolver as any).routerExplorer,
'explore',
);
const moduleName = '';
modules.set(moduleName, {});
sinon
.stub(untypedRoutesResolver.routerExplorer, 'extractRouterPath')
.stub((routesResolver as any).routerExplorer, 'extractRouterPath')
.callsFake(() => ['']);
routesResolver.registerRouters(routes, moduleName, '', '', appInstance);
@@ -139,14 +137,14 @@ describe('RoutesResolver', () => {
const appInstance = new NoopHttpAdapter(router);
const exploreSpy = sinon.spy(
untypedRoutesResolver.routerExplorer,
(routesResolver as any).routerExplorer,
'explore',
);
const moduleName = '';
modules.set(moduleName, {});
sinon
.stub(untypedRoutesResolver.routerExplorer, 'extractRouterPath')
.stub((routesResolver as any).routerExplorer, 'extractRouterPath')
.callsFake(() => ['']);
routesResolver.registerRouters(routes, moduleName, '', '', appInstance);
@@ -183,7 +181,6 @@ describe('RoutesResolver', () => {
new Injector(),
new GraphInspector(container),
);
untypedRoutesResolver = routesResolver as any;
const routes = new Map();
const routeWrapper = new InstanceWrapper({
@@ -194,14 +191,14 @@ describe('RoutesResolver', () => {
const appInstance = new NoopHttpAdapter(router);
const exploreSpy = sinon.spy(
untypedRoutesResolver.routerExplorer,
(routesResolver as any).routerExplorer,
'explore',
);
const moduleName = '';
modules.set(moduleName, {});
sinon
.stub(untypedRoutesResolver.routerExplorer, 'extractRouterPath')
.stub((routesResolver as any).routerExplorer, 'extractRouterPath')
.callsFake(() => ['']);
routesResolver.registerRouters(routes, moduleName, '', '', appInstance);

View File

@@ -56,7 +56,6 @@ describe('DependenciesScanner', () => {
class InvalidModule {}
let scanner: DependenciesScanner;
let untypedScanner: any;
let mockContainer: sinon.SinonMock;
let container: NestContainer;
let graphInspector: GraphInspector;
@@ -72,7 +71,6 @@ describe('DependenciesScanner', () => {
graphInspector,
new ApplicationConfig(),
);
untypedScanner = scanner as any;
sinon.stub(scanner, 'registerCoreModule').callsFake(async () => {});
});
@@ -88,7 +86,7 @@ describe('DependenciesScanner', () => {
.expects('replaceModule')
.never();
await scanner.scan(TestModule);
await scanner.scan(TestModule as any);
expectationCountAddModule.verify();
expectationCountReplaceModule.verify();
});
@@ -97,20 +95,20 @@ describe('DependenciesScanner', () => {
const expectation = mockContainer.expects('addProvider').twice();
const stub = sinon.stub(scanner, 'insertExportedProvider');
await scanner.scan(TestModule);
await scanner.scan(TestModule as any);
expectation.verify();
stub.restore();
});
it('should "insertController" call twice (2 components) container method "addController"', async () => {
const expectation = mockContainer.expects('addController').twice();
await scanner.scan(TestModule);
await scanner.scan(TestModule as any);
expectation.verify();
});
it('should "insertExportedProvider" call once (1 component) container method "addExportedProvider"', async () => {
const expectation = mockContainer.expects('addExportedProvider').once();
await scanner.scan(TestModule);
await scanner.scan(TestModule as any);
expectation.verify();
});
@@ -178,7 +176,7 @@ describe('DependenciesScanner', () => {
.expects('addModule')
.once();
await scanner.scan(OverrideTestModule, {
await scanner.scan(OverrideTestModule as any, {
overrides: modulesToOverride,
});
@@ -190,13 +188,13 @@ describe('DependenciesScanner', () => {
it('should "insertProvider" call once container method "addProvider"', async () => {
const expectation = mockContainer.expects('addProvider').once();
await scanner.scan(OverrideTestModule);
await scanner.scan(OverrideTestModule as any);
expectation.verify();
});
it('should "insertController" call twice (2 components) container method "addController"', async () => {
const expectation = mockContainer.expects('addController').twice();
await scanner.scan(OverrideTestModule);
await scanner.scan(OverrideTestModule as any);
expectation.verify();
});
@@ -232,7 +230,7 @@ describe('DependenciesScanner', () => {
})
class OverrideForwardRefTestModule {}
await scanner.scan(OverrideForwardRefTestModule, {
await scanner.scan(OverrideForwardRefTestModule as any, {
overrides: [
{
moduleToReplace: Overwritten,
@@ -292,7 +290,7 @@ describe('DependenciesScanner', () => {
beforeEach(() => {
addInjectableStub = sinon
.stub(untypedScanner.container, 'addInjectable')
.stub((scanner as any).container, 'addInjectable')
.callsFake(() => instanceWrapper);
insertEnhancerMetadataCacheStub = sinon
.stub(graphInspector, 'insertEnhancerMetadataCache')
@@ -429,7 +427,7 @@ describe('DependenciesScanner', () => {
const module = { forwardRef: sinon.stub().returns({}) };
sinon.stub(container, 'addImport').returns({} as any);
await scanner.insertImport(module, [] as any, 'test');
await scanner.insertImport(module as any, [] as any, 'test');
expect(module.forwardRef.called).to.be.true;
});
describe('when "related" is nil', () => {
@@ -479,7 +477,7 @@ describe('DependenciesScanner', () => {
it('should push new object to "applicationProvidersApplyMap" array', () => {
mockContainer.expects('addProvider').callsFake(() => false);
scanner.insertProvider(provider, token);
const applyMap = untypedScanner.applicationProvidersApplyMap;
const applyMap = (scanner as any).applicationProvidersApplyMap;
expect(applyMap).to.have.length(1);
expect(applyMap[0].moduleKey).to.be.eql(token);
@@ -516,11 +514,15 @@ describe('DependenciesScanner', () => {
expectation.verify();
});
it('should not push new object to "applicationProvidersApplyMap" array', () => {
expect(untypedScanner.applicationProvidersApplyMap).to.have.length(0);
expect((scanner as any).applicationProvidersApplyMap).to.have.length(
0,
);
mockContainer.expects('addProvider').callsFake(() => false);
scanner.insertProvider(component, token);
expect(untypedScanner.applicationProvidersApplyMap).to.have.length(0);
expect((scanner as any).applicationProvidersApplyMap).to.have.length(
0,
);
});
});
});
@@ -532,7 +534,7 @@ describe('DependenciesScanner', () => {
providerKey: 'providerToken',
type: APP_GUARD,
};
untypedScanner.applicationProvidersApplyMap = [provider];
(scanner as any).applicationProvidersApplyMap = [provider];
const expectedInstance = {};
const instanceWrapper = {
@@ -567,7 +569,7 @@ describe('DependenciesScanner', () => {
type: APP_GUARD,
scope: Scope.REQUEST,
};
untypedScanner.applicationProvidersApplyMap = [provider];
(scanner as any).applicationProvidersApplyMap = [provider];
const expectedInstanceWrapper = new InstanceWrapper();
mockContainer.expects('getModules').callsFake(() => ({
@@ -604,7 +606,7 @@ describe('DependenciesScanner', () => {
};
it('should add enhancers metadata to every controller and every entry provider', () => {
untypedScanner.applicationProvidersApplyMap = [provider];
(scanner as any).applicationProvidersApplyMap = [provider];
const instance = new InstanceWrapper({ name: 'test' });
const controllers = new Map();
@@ -649,7 +651,7 @@ describe('DependenciesScanner', () => {
describe(`when token is ${APP_INTERCEPTOR}`, () => {
it('call "addGlobalInterceptor"', () => {
const addSpy = sinon.spy(
untypedScanner.applicationConfig,
(scanner as any).applicationConfig,
'addGlobalInterceptor',
);
scanner.getApplyProvidersMap()[APP_INTERCEPTOR](null);
@@ -659,7 +661,7 @@ describe('DependenciesScanner', () => {
describe(`when token is ${APP_GUARD}`, () => {
it('call "addGlobalGuard"', () => {
const addSpy = sinon.spy(
untypedScanner.applicationConfig,
(scanner as any).applicationConfig,
'addGlobalGuard',
);
scanner.getApplyProvidersMap()[APP_GUARD](null);
@@ -669,7 +671,7 @@ describe('DependenciesScanner', () => {
describe(`when token is ${APP_PIPE}`, () => {
it('call "addGlobalPipe"', () => {
const addSpy = sinon.spy(
untypedScanner.applicationConfig,
(scanner as any).applicationConfig,
'addGlobalPipe',
);
scanner.getApplyProvidersMap()[APP_PIPE](null);
@@ -679,7 +681,7 @@ describe('DependenciesScanner', () => {
describe(`when token is ${APP_FILTER}`, () => {
it('call "addGlobalFilter"', () => {
const addSpy = sinon.spy(
untypedScanner.applicationConfig,
(scanner as any).applicationConfig,
'addGlobalFilter',
);
scanner.getApplyProvidersMap()[APP_FILTER](null);
@@ -691,7 +693,7 @@ describe('DependenciesScanner', () => {
describe(`when token is ${APP_INTERCEPTOR}`, () => {
it('call "addGlobalRequestInterceptor"', () => {
const addSpy = sinon.spy(
untypedScanner.applicationConfig,
(scanner as any).applicationConfig,
'addGlobalRequestInterceptor',
);
scanner.getApplyRequestProvidersMap()[APP_INTERCEPTOR](null);
@@ -701,7 +703,7 @@ describe('DependenciesScanner', () => {
describe(`when token is ${APP_GUARD}`, () => {
it('call "addGlobalRequestGuard"', () => {
const addSpy = sinon.spy(
untypedScanner.applicationConfig,
(scanner as any).applicationConfig,
'addGlobalRequestGuard',
);
scanner.getApplyRequestProvidersMap()[APP_GUARD](null);
@@ -711,7 +713,7 @@ describe('DependenciesScanner', () => {
describe(`when token is ${APP_PIPE}`, () => {
it('call "addGlobalRequestPipe"', () => {
const addSpy = sinon.spy(
untypedScanner.applicationConfig,
(scanner as any).applicationConfig,
'addGlobalRequestPipe',
);
scanner.getApplyRequestProvidersMap()[APP_PIPE](null);
@@ -721,7 +723,7 @@ describe('DependenciesScanner', () => {
describe(`when token is ${APP_FILTER}`, () => {
it('call "addGlobalRequestFilter"', () => {
const addSpy = sinon.spy(
untypedScanner.applicationConfig,
(scanner as any).applicationConfig,
'addGlobalRequestFilter',
);
scanner.getApplyRequestProvidersMap()[APP_FILTER](null);

View File

@@ -5,63 +5,48 @@ const transformDecorator = Reflector.createDecorator<string[], number>({
transform: value => value.length,
});
type TestObject = {
only1?: string;
only2?: string;
both: string;
};
describe('Reflector', () => {
const key = 'key';
let reflector: Reflector;
class Test {}
@transformDecorator(['a', 'b', 'c'])
class TestTransform {}
class Test {}
class Test1 {}
class Test2 {}
beforeEach(() => {
Reflect.deleteMetadata(key, Test1);
Reflect.deleteMetadata(key, Test2);
reflector = new Reflector();
});
describe('get', () => {
it('should reflect metadata by key', () => {
const key = 'key';
const value = 'value';
Reflect.defineMetadata(key, value, Test1);
expect(reflector.get(key, Test1)).to.eql(value);
Reflect.defineMetadata(key, value, Test);
expect(reflector.get(key, Test)).to.eql(value);
});
it('should reflect metadata by decorator', () => {
const decorator = Reflector.createDecorator<string>();
const value = 'value';
Reflect.defineMetadata(decorator.KEY, value, Test1);
Reflect.defineMetadata(decorator.KEY, value, Test);
// string
let reflectedValue = reflector.get(decorator, Test1);
let reflectedValue = reflector.get(decorator, Test);
expect(reflectedValue).to.eql(value);
// @ts-expect-error 'value' is not assignable to parameter of type 'string'
reflectedValue = true;
reflectedValue satisfies string;
});
it('should reflect metadata by decorator (custom key)', () => {
const decorator = Reflector.createDecorator<string[]>({ key: 'custom' });
const value = ['value'];
Reflect.defineMetadata('custom', value, Test1);
Reflect.defineMetadata('custom', value, Test);
// string[]
let reflectedValue = reflector.get(decorator, Test1);
let reflectedValue = reflector.get(decorator, Test);
expect(reflectedValue).to.eql(value);
// @ts-expect-error 'value' is not assignable to parameter of type 'string[]'
reflectedValue = true;
reflectedValue satisfies string[];
});
it('should reflect metadata by decorator (with transform option)', () => {
@@ -70,8 +55,6 @@ describe('Reflector', () => {
// @ts-expect-error 'value' is not assignable to type 'number'
reflectedValue = [];
reflectedValue satisfies number;
});
it('should require transform option when second generic type is provided', () => {
@@ -81,121 +64,53 @@ describe('Reflector', () => {
});
describe('getAll', () => {
it('should reflect metadata of all targets by key', () => {
const value1 = 'value1';
const value2 = 'value2';
Reflect.defineMetadata(key, value1, Test1);
Reflect.defineMetadata(key, value2, Test2);
expect(reflector.getAll(key, [Test1, Test2])).to.eql([value1, value2]);
});
it('should reflect metadata of all targets by decorator', () => {
const decorator = Reflector.createDecorator<string>();
const value1 = 'value1';
const value2 = 'value2';
Reflect.defineMetadata(decorator.KEY, value1, Test1);
Reflect.defineMetadata(decorator.KEY, value2, Test2);
// string[]
const reflectedValue = reflector.getAll(decorator, [Test1, Test2]);
expect(reflectedValue).to.eql([value1, value2]);
reflectedValue satisfies string[];
it('should reflect metadata of all targets', () => {
const key = 'key';
const value = 'value';
Reflect.defineMetadata(key, value, Test);
expect(reflector.getAll(key, [Test])).to.eql([value]);
});
});
describe('getAllAndMerge', () => {
it('should return an empty array when there are no targets', () => {
const key = 'key';
expect(reflector.getAllAndMerge(key, [])).to.be.empty;
});
it('should reflect metadata of all targets and concat arrays', () => {
const decorator = Reflector.createDecorator<string[]>();
const key = 'key';
const value = 'value';
Reflect.defineMetadata(decorator.KEY, [value], Test1);
// string[]
const reflectedValue = reflector.getAllAndMerge(decorator, [
Test1,
Test1,
Reflect.defineMetadata(key, [value], Test);
expect(reflector.getAllAndMerge(key, [Test, Test])).to.eql([
value,
value,
]);
expect(reflectedValue).to.eql([value, value]);
reflectedValue satisfies string[];
});
it('should reflect metadata of all targets and concat boolean arrays', () => {
const decorator = Reflector.createDecorator<boolean>();
const value = true;
Reflect.defineMetadata(decorator.KEY, [value], Test1);
// string[]
const reflectedValue = reflector.getAllAndMerge(decorator, [
Test1,
Test1,
]);
expect(reflectedValue).to.eql([value, value]);
reflectedValue satisfies boolean[];
});
it('should reflect metadata of all targets and create an array', () => {
const decorator = Reflector.createDecorator<string>();
const key = 'key';
const value = 'value';
Reflect.defineMetadata(decorator.KEY, value, Test1);
// string[]
const reflectedValue = reflector.getAllAndMerge(decorator, [
Test1,
Test1,
Reflect.defineMetadata(key, value, Test);
expect(reflector.getAllAndMerge(key, [Test, Test])).to.eql([
value,
value,
]);
expect(reflectedValue).to.eql([value, value]);
reflectedValue satisfies string[];
});
it('should reflect metadata of all targets and merge objects', () => {
const decorator = Reflector.createDecorator<TestObject>();
const value1: TestObject = { only1: 'test1', both: 'overriden' };
const value2: TestObject = { only2: 'test2', both: 'test' };
Reflect.defineMetadata(decorator.KEY, value1, Test1);
Reflect.defineMetadata(decorator.KEY, value2, Test2);
// TestObject
const reflectedValue = reflector.getAllAndMerge(decorator, [
Test1,
Test2,
]);
expect(reflectedValue).to.eql({
...value1,
...value2,
it('should reflect metadata of all targets and merge an object', () => {
const key = 'key';
const value = { test: 'test' };
Reflect.defineMetadata(key, value, Test);
expect(reflector.getAllAndMerge(key, [Test, Test])).to.eql({
...value,
});
reflectedValue satisfies TestObject;
});
it('should reflect metadata of all targets and create an array from a single value', () => {
const value = 'value';
Reflect.defineMetadata(key, value, Test1);
const result = reflector.getAllAndMerge(key, [Test1, Test2]);
expect(result).to.eql([value]);
result satisfies string[];
});
it('should reflect metadata of all targets and return a single array unmodified', () => {
const value = ['value'];
Reflect.defineMetadata(key, value, Test1);
expect(reflector.getAllAndMerge(key, [Test1, Test2])).to.eql(value);
});
it('should reflect metadata of all targets and return a single object unmodified', () => {
const value = { test: 'value' };
Reflect.defineMetadata(key, value, Test1);
expect(reflector.getAllAndMerge(key, [Test1, Test2])).to.eql(value);
});
});
describe('getAllAndOverride', () => {
it('should reflect metadata of all targets and return a first not undefined value', () => {
const value1 = 'value1';
const value2 = 'value2';
Reflect.defineMetadata(key, value1, Test1);
Reflect.defineMetadata(key, value2, Test2);
expect(reflector.getAllAndOverride(key, [Test1, Test2])).to.eql(value1);
const key = 'key';
const value = 'value';
Reflect.defineMetadata(key, value, Test);
expect(reflector.getAllAndOverride(key, [Test, Test])).to.eql(value);
});
});
});

View File

@@ -94,7 +94,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td><a href="https://www.mercedes-benz.com/" target="_blank"><img src="https://nestjs.com/img/logos/mercedes-logo.png" width="100" valign="middle" /></a></td>
<td><a href="https://www.dinii.jp/" target="_blank"><img src="https://nestjs.com/img/logos/dinii-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://bloodycase.com/?promocode=NEST" target="_blank"><img src="https://nestjs.com/img/logos/bloodycase-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-logo.svg" width="150" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-dark-logo.svg#2" width="150" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
</tr>

View File

@@ -6,42 +6,23 @@ import { GRPC_DEFAULT_PROTO_LOADER, GRPC_DEFAULT_URL } from '../constants';
import { InvalidGrpcPackageException } from '../errors/invalid-grpc-package.exception';
import { InvalidGrpcServiceException } from '../errors/invalid-grpc-service.exception';
import { InvalidProtoDefinitionException } from '../errors/invalid-proto-definition.exception';
import { ChannelOptions } from '../external/grpc-options.interface';
import { getGrpcPackageDefinition } from '../helpers';
import { ClientGrpc, GrpcOptions } from '../interfaces';
import { ClientProxy } from './client-proxy';
import { GRPC_CANCELLED } from './constants';
import { ChannelOptions } from '../external/grpc-options.interface';
import { getGrpcPackageDefinition } from '../helpers';
const GRPC_CANCELLED = 'Cancelled';
// To enable type safety for gRPC. This cant be uncommented by default
// because it would require the user to install the @grpc/grpc-js package even if they dont use gRPC
// Otherwise, TypeScript would fail to compile the code.
//
// type GrpcClient = import('@grpc/grpc-js').Client;
// let grpcPackage = {} as typeof import('@grpc/grpc-js');
// let grpcProtoLoaderPackage = {} as typeof import('@grpc/proto-loader');
type GrpcClient = any;
let grpcPackage = {} as any;
let grpcProtoLoaderPackage = {} as any;
let grpcPackage: any = {};
let grpcProtoLoaderPackage: any = {};
/**
* @publicApi
*/
export class ClientGrpcProxy
extends ClientProxy<never, never>
implements ClientGrpc
{
export class ClientGrpcProxy extends ClientProxy implements ClientGrpc {
protected readonly logger = new Logger(ClientProxy.name);
protected readonly clients = new Map<string, any>();
protected readonly url: string;
protected grpcClients: GrpcClient[] = [];
get status(): never {
throw new Error(
'The "status" attribute is not supported by the gRPC transport',
);
}
protected grpcClients = [];
constructor(protected readonly options: GrpcOptions['options']) {
super();
@@ -386,15 +367,4 @@ export class ClientGrpcProxy
'Method is not supported in gRPC mode. Use ClientGrpc instead (learn more in the documentation).',
);
}
public on<EventKey extends never = never, EventCallback = any>(
event: EventKey,
callback: EventCallback,
) {
throw new Error('Method is not supported in gRPC mode.');
}
public unwrap<T>(): T {
throw new Error('Method is not supported in gRPC mode.');
}
}

View File

@@ -1,6 +1,6 @@
import { Logger } from '@nestjs/common/services/logger.service';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { isUndefined } from '@nestjs/common/utils/shared.utils';
import { isNil, isUndefined } from '@nestjs/common/utils/shared.utils';
import {
KAFKA_DEFAULT_BROKER,
KAFKA_DEFAULT_CLIENT,
@@ -9,7 +9,6 @@ import {
import { KafkaResponseDeserializer } from '../deserializers/kafka-response.deserializer';
import { KafkaHeaders } from '../enums';
import { InvalidKafkaClientTopicException } from '../errors/invalid-kafka-client-topic.exception';
import { KafkaStatus } from '../events';
import {
BrokersFunction,
Consumer,
@@ -28,9 +27,7 @@ import {
KafkaReplyPartitionAssigner,
} from '../helpers';
import {
ClientKafkaProxy,
KafkaOptions,
MsPattern,
OutgoingEvent,
ReadPacket,
WritePacket,
@@ -40,18 +37,26 @@ import {
KafkaRequestSerializer,
} from '../serializers/kafka-request.serializer';
import { ClientProxy } from './client-proxy';
import {
connectable,
defer,
Observable,
Subject,
throwError as _throw,
} from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { InvalidMessageException } from '../errors/invalid-message.exception';
let kafkaPackage: any = {};
/**
* @publicApi
*/
export class ClientKafka
extends ClientProxy<never, KafkaStatus>
implements ClientKafkaProxy
{
export class ClientKafka extends ClientProxy {
protected logger = new Logger(ClientKafka.name);
protected client: Kafka | null = null;
protected consumer: Consumer | null = null;
protected producer: Producer | null = null;
protected parser: KafkaParser | null = null;
protected initialized: Promise<void> | null = null;
protected responsePatterns: string[] = [];
@@ -60,26 +65,6 @@ export class ClientKafka
protected clientId: string;
protected groupId: string;
protected producerOnlyMode: boolean;
protected _consumer: Consumer | null = null;
protected _producer: Producer | null = null;
get consumer(): Consumer {
if (!this._consumer) {
throw new Error(
'No consumer initialized. Please, call the "connect" method first.',
);
}
return this._consumer;
}
get producer(): Producer {
if (!this._consumer) {
throw new Error(
'No producer initialized. Please, call the "connect" method first.',
);
}
return this._producer;
}
constructor(protected readonly options: KafkaOptions['options']) {
super();
@@ -119,27 +104,28 @@ export class ClientKafka
this.initializeDeserializer(options);
}
public subscribeToResponseOf(pattern: unknown): void {
const request = this.normalizePattern(pattern as MsPattern);
public subscribeToResponseOf(pattern: any): void {
const request = this.normalizePattern(pattern);
this.responsePatterns.push(this.getResponsePatternName(request));
}
public async close(): Promise<void> {
this._producer && (await this._producer.disconnect());
this._consumer && (await this._consumer.disconnect());
this._producer = null;
this._consumer = null;
this.producer && (await this.producer.disconnect());
this.consumer && (await this.consumer.disconnect());
this.producer = null;
this.consumer = null;
this.initialized = null;
this.client = null;
}
public async connect(): Promise<Producer> {
if (this.initialized) {
return this.initialized.then(() => this._producer);
return this.initialized.then(() => this.producer);
}
this.initialized = new Promise(async (resolve, reject) => {
try {
this.client = this.createClient();
if (!this.producerOnlyMode) {
const partitionAssigners = [
(
@@ -159,45 +145,42 @@ export class ClientKafka
},
);
this._consumer = this.client.consumer(consumerOptions);
this.registerConsumerEventListeners();
// Set member assignments on join and rebalance
this._consumer.on(
this._consumer.events.GROUP_JOIN,
this.consumer = this.client.consumer(consumerOptions);
// set member assignments on join and rebalance
this.consumer.on(
this.consumer.events.GROUP_JOIN,
this.setConsumerAssignments.bind(this),
);
await this._consumer.connect();
await this.consumer.connect();
await this.bindTopics();
}
this._producer = this.client.producer(this.options.producer || {});
this.registerProducerEventListeners();
await this._producer.connect();
this.producer = this.client.producer(this.options.producer || {});
await this.producer.connect();
resolve();
} catch (err) {
reject(err);
}
});
return this.initialized.then(() => this._producer);
return this.initialized.then(() => this.producer);
}
public async bindTopics(): Promise<void> {
if (!this._consumer) {
if (!this.consumer) {
throw Error('No consumer initialized');
}
const consumerSubscribeOptions = this.options.subscribe || {};
if (this.responsePatterns.length > 0) {
await this._consumer.subscribe({
await this.consumer.subscribe({
...consumerSubscribeOptions,
topics: this.responsePatterns,
});
}
await this._consumer.run(
await this.consumer.run(
Object.assign(this.options.run || {}, {
eachMessage: this.createResponseCallback(),
}),
@@ -249,50 +232,56 @@ export class ClientKafka
return this.consumerAssignments;
}
public emitBatch<TResult = any, TInput = any>(
pattern: any,
data: { messages: TInput[] },
): Observable<TResult> {
if (isNil(pattern) || isNil(data)) {
return _throw(() => new InvalidMessageException());
}
const source = defer(async () => this.connect()).pipe(
mergeMap(() => this.dispatchBatchEvent({ pattern, data })),
);
const connectableSource = connectable(source, {
connector: () => new Subject(),
resetOnDisconnect: false,
});
connectableSource.connect();
return connectableSource;
}
public commitOffsets(
topicPartitions: TopicPartitionOffsetAndMetadata[],
): Promise<void> {
if (this._consumer) {
return this._consumer.commitOffsets(topicPartitions);
if (this.consumer) {
return this.consumer.commitOffsets(topicPartitions);
} else {
throw new Error('No consumer initialized');
}
}
public unwrap<T>(): T {
if (!this.client) {
throw new Error(
'Not initialized. Please call the "connect" method first.',
);
protected async dispatchBatchEvent<TInput = any>(
packets: ReadPacket<{ messages: TInput[] }>,
): Promise<any> {
if (packets.data.messages.length === 0) {
return;
}
return this.client as T;
}
const pattern = this.normalizePattern(packets.pattern);
const outgoingEvents = await Promise.all(
packets.data.messages.map(message => {
return this.serializer.serialize(message as any, { pattern });
}),
);
protected registerConsumerEventListeners() {
this._consumer.on(this._consumer.events.CONNECT, () =>
this._status$.next(KafkaStatus.CONNECTED),
const message = Object.assign(
{
topic: pattern,
messages: outgoingEvents,
},
this.options.send || {},
);
this._consumer.on(this._consumer.events.DISCONNECT, () =>
this._status$.next(KafkaStatus.DISCONNECTED),
);
this._consumer.on(this._consumer.events.REBALANCING, () =>
this._status$.next(KafkaStatus.REBALANCING),
);
this._consumer.on(this._consumer.events.STOP, () =>
this._status$.next(KafkaStatus.STOPPED),
);
this.consumer.on(this._consumer.events.CRASH, () =>
this._status$.next(KafkaStatus.CRASHED),
);
}
protected registerProducerEventListeners() {
this._producer.on(this._producer.events.CONNECT, () =>
this._status$.next(KafkaStatus.CONNECTED),
);
this._producer.on(this._producer.events.DISCONNECT, () =>
this._status$.next(KafkaStatus.DISCONNECTED),
);
return this.producer.send(message);
}
protected async dispatchEvent(packet: OutgoingEvent): Promise<any> {
@@ -308,7 +297,7 @@ export class ClientKafka
this.options.send || {},
);
return this._producer.send(message);
return this.producer.send(message);
}
protected getReplyTopicPartition(topic: string): string {
@@ -317,7 +306,7 @@ export class ClientKafka
throw new InvalidKafkaClientTopicException(topic);
}
// Get the minimum partition
// get the minimum partition
return minimumPartition.toString();
}
@@ -354,7 +343,7 @@ export class ClientKafka
this.options.send || {},
);
return this._producer.send(message);
return this.producer.send(message);
})
.catch(err => errorCallback(err));
@@ -371,7 +360,7 @@ export class ClientKafka
protected setConsumerAssignments(data: ConsumerGroupJoinEvent): void {
const consumerAssignments: { [key: string]: number } = {};
// Only need to set the minimum
// only need to set the minimum
Object.keys(data.payload.memberAssignment).forEach(topic => {
const memberPartitions = data.payload.memberAssignment[topic];
@@ -392,11 +381,4 @@ export class ClientKafka
this.deserializer =
(options && options.deserializer) || new KafkaResponseDeserializer();
}
public on<
EventKey extends string | number | symbol = string | number | symbol,
EventCallback = any,
>(event: EventKey, callback: EventCallback) {
throw new Error('Method is not supported for Kafka client');
}
}

View File

@@ -2,8 +2,14 @@ import { Logger } from '@nestjs/common/services/logger.service';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { EmptyError, fromEvent, lastValueFrom, merge, Observable } from 'rxjs';
import { first, map, share, tap } from 'rxjs/operators';
import { ECONNREFUSED, MQTT_DEFAULT_URL } from '../constants';
import { MqttEvents, MqttEventsMap, MqttStatus } from '../events/mqtt.events';
import {
CLOSE_EVENT,
ECONNREFUSED,
ERROR_EVENT,
MESSAGE_EVENT,
MQTT_DEFAULT_URL,
} from '../constants';
import { MqttClient } from '../external/mqtt-client.interface';
import { MqttOptions, ReadPacket, WritePacket } from '../interfaces';
import {
MqttRecord,
@@ -14,32 +20,19 @@ import { ClientProxy } from './client-proxy';
let mqttPackage: any = {};
// To enable type safety for MQTT. This cant be uncommented by default
// because it would require the user to install the mqtt package even if they dont use MQTT
// Otherwise, TypeScript would fail to compile the code.
//
type MqttClient = import('mqtt').MqttClient;
// type MqttClient = any;
/**
* @publicApi
*/
export class ClientMqtt extends ClientProxy<MqttEvents, MqttStatus> {
export class ClientMqtt extends ClientProxy {
protected readonly logger = new Logger(ClientProxy.name);
protected readonly subscriptionsCount = new Map<string, number>();
protected readonly url: string;
protected mqttClient: MqttClient;
protected connectionPromise: Promise<any>;
protected isInitialConnection = false;
protected isReconnecting = false;
protected pendingEventListeners: Array<{
event: keyof MqttEvents;
callback: MqttEvents[keyof MqttEvents];
}> = [];
protected connection: Promise<any>;
constructor(protected readonly options: MqttOptions['options']) {
super();
this.url = this.getOptionsProp(this.options, 'url') ?? MQTT_DEFAULT_URL;
this.url = this.getOptionsProp(this.options, 'url') || MQTT_DEFAULT_URL;
mqttPackage = loadPackage('mqtt', ClientMqtt.name, () => require('mqtt'));
@@ -58,49 +51,38 @@ export class ClientMqtt extends ClientProxy<MqttEvents, MqttStatus> {
public close() {
this.mqttClient && this.mqttClient.end();
this.mqttClient = null;
this.connectionPromise = null;
this.pendingEventListeners = [];
this.connection = null;
}
public connect(): Promise<any> {
if (this.mqttClient) {
return this.connectionPromise;
return this.connection;
}
this.mqttClient = this.createClient();
this.registerErrorListener(this.mqttClient);
this.registerOfflineListener(this.mqttClient);
this.registerReconnectListener(this.mqttClient);
this.registerConnectListener(this.mqttClient);
this.registerDisconnectListener(this.mqttClient);
this.registerCloseListener(this.mqttClient);
this.pendingEventListeners.forEach(({ event, callback }) =>
this.mqttClient.on(event, callback),
);
this.pendingEventListeners = [];
this.handleError(this.mqttClient);
const connect$ = this.connect$(this.mqttClient);
this.connectionPromise = lastValueFrom(
this.mergeCloseEvent(this.mqttClient, connect$).pipe(share()),
this.connection = lastValueFrom(
this.mergeCloseEvent(this.mqttClient, connect$).pipe(
tap(() =>
this.mqttClient.on(MESSAGE_EVENT, this.createResponseCallback()),
),
share(),
),
).catch(err => {
if (err instanceof EmptyError) {
return;
}
throw err;
});
return this.connectionPromise;
return this.connection;
}
public mergeCloseEvent<T = any>(
instance: MqttClient,
source$: Observable<T>,
): Observable<T> {
const close$ = fromEvent(instance, MqttEventsMap.CLOSE).pipe(
tap({
next: () => {
this._status$.next(MqttStatus.CLOSED);
},
}),
const close$ = fromEvent(instance, CLOSE_EVENT).pipe(
map((err: any) => {
throw err;
}),
@@ -112,81 +94,13 @@ export class ClientMqtt extends ClientProxy<MqttEvents, MqttStatus> {
return mqttPackage.connect(this.url, this.options as MqttOptions);
}
public registerErrorListener(client: MqttClient) {
client.on(
MqttEventsMap.ERROR,
public handleError(client: MqttClient) {
client.addListener(
ERROR_EVENT,
(err: any) => err.code !== ECONNREFUSED && this.logger.error(err),
);
}
public registerOfflineListener(client: MqttClient) {
client.on(MqttEventsMap.OFFLINE, () => {
this.connectionPromise = Promise.reject(
'Error: Connection lost. Trying to reconnect...',
);
// Prevent unhandled rejections
this.connectionPromise.catch(() => {});
this.logger.error('MQTT broker went offline.');
});
}
public registerReconnectListener(client: MqttClient) {
client.on(MqttEventsMap.RECONNECT, () => {
this.isReconnecting = true;
this._status$.next(MqttStatus.RECONNECTING);
this.logger.log('MQTT connection lost. Trying to reconnect...');
});
}
public registerDisconnectListener(client: MqttClient) {
client.on(MqttEventsMap.DISCONNECT, () => {
this._status$.next(MqttStatus.DISCONNECTED);
});
}
public registerCloseListener(client: MqttClient) {
client.on(MqttEventsMap.CLOSE, () => {
this._status$.next(MqttStatus.CLOSED);
});
}
public registerConnectListener(client: MqttClient) {
client.on(MqttEventsMap.CONNECT, () => {
this.isReconnecting = false;
this._status$.next(MqttStatus.CONNECTED);
this.logger.log('Connected to MQTT broker');
this.connectionPromise = Promise.resolve();
if (!this.isInitialConnection) {
this.isInitialConnection = true;
client.on('message', this.createResponseCallback());
}
});
}
public on<
EventKey extends keyof MqttEvents = keyof MqttEvents,
EventCallback extends MqttEvents[EventKey] = MqttEvents[EventKey],
>(event: EventKey, callback: EventCallback) {
if (this.mqttClient) {
this.mqttClient.on(event, callback as any);
} else {
this.pendingEventListeners.push({ event, callback });
}
}
public unwrap<T>(): T {
if (!this.mqttClient) {
throw new Error(
'Not initialized. Please call the "connect" method first.',
);
}
return this.mqttClient as T;
}
public createResponseCallback(): (channel: string, buffer: Buffer) => any {
return async (channel: string, buffer: Buffer) => {
const packet = JSON.parse(buffer.toString());
@@ -296,15 +210,22 @@ export class ClientMqtt extends ClientProxy<MqttEvents, MqttStatus> {
return undefined;
}
return {
...requestOptions,
properties: {
...requestOptions?.properties,
// Cant just spread objects as MQTT won't deliver
// any message with empty object as "userProperties" field
// @url https://github.com/nestjs/nest/issues/14079
let options: MqttRecordOptions = {};
if (requestOptions) {
options = { ...requestOptions };
}
if (this.options?.userProperties) {
options.properties = {
...options.properties,
userProperties: {
...this.options?.userProperties,
...requestOptions?.properties?.userProperties,
...options.properties?.userProperties,
},
},
};
};
}
return options;
}
}

View File

@@ -1,11 +1,10 @@
import { Logger } from '@nestjs/common/services/logger.service';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { isObject } from '@nestjs/common/utils/shared.utils';
import { EventEmitter } from 'stream';
import { NATS_DEFAULT_URL } from '../constants';
import { NatsResponseJSONDeserializer } from '../deserializers/nats-response-json.deserializer';
import { EmptyResponseException } from '../errors/empty-response.exception';
import { NatsEvents, NatsEventsMap, NatsStatus } from '../events/nats.events';
import { Client, NatsMsg } from '../external/nats-client.interface';
import { NatsOptions, PacketId, ReadPacket, WritePacket } from '../interfaces';
import { NatsRecord } from '../record-builders';
import { NatsRecordSerializer } from '../serializers/nats-record.serializer';
@@ -13,27 +12,13 @@ import { ClientProxy } from './client-proxy';
let natsPackage = {} as any;
// To enable type safety for Nats. This cant be uncommented by default
// because it would require the user to install the nats package even if they dont use Nats
// Otherwise, TypeScript would fail to compile the code.
//
// type Client = import('nats').NatsConnection;
// type NatsMsg = import('nats').Msg;
type Client = any;
type NatsMsg = any;
/**
* @publicApi
*/
export class ClientNats extends ClientProxy<NatsEvents, NatsStatus> {
export class ClientNats extends ClientProxy {
protected readonly logger = new Logger(ClientNats.name);
protected natsClient: Client;
protected connectionPromise: Promise<Client>;
protected statusEventEmitter = new EventEmitter<{
[key in keyof NatsEvents]: Parameters<NatsEvents[key]>;
}>();
protected clientConnectionPromise: Promise<Client>;
constructor(protected readonly options: NatsOptions['options']) {
super();
@@ -45,29 +30,22 @@ export class ClientNats extends ClientProxy<NatsEvents, NatsStatus> {
public async close() {
await this.natsClient?.close();
this.statusEventEmitter.removeAllListeners();
this.natsClient = null;
this.connectionPromise = null;
this.clientConnectionPromise = null;
}
public async connect(): Promise<any> {
if (this.connectionPromise) {
return this.connectionPromise;
if (this.clientConnectionPromise) {
return this.clientConnectionPromise;
}
this.connectionPromise = this.createClient();
this.natsClient = await this.connectionPromise.catch(err => {
this.connectionPromise = null;
throw err;
});
this._status$.next(NatsStatus.CONNECTED);
this.clientConnectionPromise = this.createClient();
this.natsClient = await this.clientConnectionPromise;
this.handleStatusUpdates(this.natsClient);
return this.natsClient;
}
public createClient(): Promise<Client> {
const options = this.options || ({} as NatsOptions);
const options: any = this.options || ({} as NatsOptions);
return natsPackage.connect({
servers: NATS_DEFAULT_URL,
...options,
@@ -83,44 +61,10 @@ export class ClientNats extends ClientProxy<NatsEvents, NatsStatus> {
switch (status.type) {
case 'error':
this.logger.error(
`NatsError: type: "${status.type}", data: "${data}".`,
);
break;
case 'disconnect':
this.connectionPromise = Promise.reject(
'Error: Connection lost. Trying to reconnect...',
);
// Prevent unhandled promise rejection
this.connectionPromise.catch(() => {});
this.logger.error(
`NatsError: type: "${status.type}", data: "${data}".`,
);
this._status$.next(NatsStatus.DISCONNECTED);
this.statusEventEmitter.emit(
NatsEventsMap.DISCONNECT,
status.data as string,
);
break;
case 'reconnecting':
this._status$.next(NatsStatus.RECONNECTING);
break;
case 'reconnect':
this.connectionPromise = Promise.resolve(client);
this.logger.log(
`NatsStatus: type: "${status.type}", data: "${data}".`,
);
this._status$.next(NatsStatus.CONNECTED);
this.statusEventEmitter.emit(
NatsEventsMap.RECONNECT,
status.data as string,
);
break;
case 'pingTimer':
@@ -131,13 +75,6 @@ export class ClientNats extends ClientProxy<NatsEvents, NatsStatus> {
}
break;
case 'update':
this.logger.log(
`NatsStatus: type: "${status.type}", data: "${data}".`,
);
this.statusEventEmitter.emit(NatsEventsMap.UPDATE, status.data);
break;
default:
this.logger.log(
`NatsStatus: type: "${status.type}", data: "${data}".`,
@@ -147,22 +84,6 @@ export class ClientNats extends ClientProxy<NatsEvents, NatsStatus> {
}
}
public on<
EventKey extends keyof NatsEvents = keyof NatsEvents,
EventCallback extends NatsEvents[EventKey] = NatsEvents[EventKey],
>(event: EventKey, callback: EventCallback) {
this.statusEventEmitter.on(event, callback as any);
}
public unwrap<T>(): T {
if (!this.natsClient) {
throw new Error(
'Not initialized. Please call the "connect" method first.',
);
}
return this.natsClient as T;
}
public createSubscriptionHandler(
packet: ReadPacket & PacketId,
callback: (packet: WritePacket) => any,

View File

@@ -1,10 +1,10 @@
import { Transport } from '../enums/transport.enum';
import { ClientKafkaProxy } from '../interfaces';
import {
ClientOptions,
CustomClientOptions,
TcpClientOptions,
} from '../interfaces/client-metadata.interface';
import { Closeable } from '../interfaces/closeable.interface';
import {
GrpcOptions,
KafkaOptions,
@@ -23,7 +23,7 @@ import { ClientRMQ } from './client-rmq';
import { ClientTCP } from './client-tcp';
export interface IClientProxyFactory {
create(clientOptions: ClientOptions): ClientProxy;
create(clientOptions: ClientOptions): ClientProxy & Closeable;
}
/**
@@ -33,38 +33,33 @@ export class ClientProxyFactory {
public static create(
clientOptions: { transport: Transport.GRPC } & ClientOptions,
): ClientGrpcProxy;
public static create(clientOptions: ClientOptions): ClientProxy & Closeable;
public static create(
clientOptions: { transport: Transport.KAFKA } & ClientOptions,
): ClientKafkaProxy;
public static create(clientOptions: ClientOptions): ClientProxy;
public static create(clientOptions: CustomClientOptions): ClientProxy;
clientOptions: CustomClientOptions,
): ClientProxy & Closeable;
public static create(
clientOptions: ClientOptions | CustomClientOptions,
): ClientProxy | ClientGrpcProxy | ClientKafkaProxy {
): ClientProxy & Closeable {
if (this.isCustomClientOptions(clientOptions)) {
const { customClass, options } = clientOptions;
return new customClass(options);
}
const { transport, options = {} } = clientOptions ?? { options: {} };
const { transport, options } = clientOptions || {};
switch (transport) {
case Transport.REDIS:
return new ClientRedis(
options as RedisOptions['options'],
) as ClientProxy;
return new ClientRedis(options as RedisOptions['options']);
case Transport.NATS:
return new ClientNats(options as NatsOptions['options']) as ClientProxy;
return new ClientNats(options as NatsOptions['options']);
case Transport.MQTT:
return new ClientMqtt(options as MqttOptions['options']) as ClientProxy;
return new ClientMqtt(options as MqttOptions['options']);
case Transport.GRPC:
return new ClientGrpcProxy(options as GrpcOptions['options']);
case Transport.RMQ:
return new ClientRMQ(options as RmqOptions['options']) as ClientProxy;
return new ClientRMQ(options as RmqOptions['options']);
case Transport.KAFKA:
return new ClientKafka(options as KafkaOptions['options']);
default:
return new ClientTCP(
options as TcpClientOptions['options'],
) as ClientProxy;
return new ClientTCP(options as TcpClientOptions['options']);
}
}

View File

@@ -1,17 +1,17 @@
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { isNil } from '@nestjs/common/utils/shared.utils';
import {
throwError as _throw,
connectable,
defer,
fromEvent,
merge,
Observable,
Observer,
ReplaySubject,
Subject,
throwError as _throw,
} from 'rxjs';
import { distinctUntilChanged, map, mergeMap, take } from 'rxjs/operators';
import { map, mergeMap, take } from 'rxjs/operators';
import { CONNECT_EVENT, ERROR_EVENT } from '../constants';
import { IncomingResponseDeserializer } from '../deserializers/incoming-response.deserializer';
import { InvalidMessageException } from '../errors/invalid-message.exception';
import {
@@ -32,57 +32,14 @@ import { ProducerSerializer } from '../interfaces/serializer.interface';
import { IdentitySerializer } from '../serializers/identity.serializer';
import { transformPatternToRoute } from '../utils';
/**
* @publicApi
*/
export abstract class ClientProxy<
EventsMap extends Record<never, Function> = Record<never, Function>,
Status extends string = string,
> {
export abstract class ClientProxy {
public abstract connect(): Promise<any>;
public abstract close(): any;
protected routingMap = new Map<string, Function>();
protected serializer: ProducerSerializer;
protected deserializer: ProducerDeserializer;
protected _status$ = new ReplaySubject<Status>(1);
/**
* Returns an observable that emits status changes.
*/
public get status(): Observable<Status> {
return this._status$.asObservable().pipe(distinctUntilChanged());
}
/**
* Establishes the connection to the underlying server/broker.
*/
public abstract connect(): Promise<any>;
/**
* Closes the underlying connection to the server/broker.
*/
public abstract close(): any;
/**
* Registers an event listener for the given event.
* @param event Event name
* @param callback Callback to be executed when the event is emitted
*/
public on<
EventKey extends keyof EventsMap = keyof EventsMap,
EventCallback extends EventsMap[EventKey] = EventsMap[EventKey],
>(event: EventKey, callback: EventCallback) {
throw new Error('Method not implemented.');
}
/**
* Returns an instance of the underlying server/broker instance,
* or a group of servers if there are more than one.
*/
public abstract unwrap<T>(): T;
/**
* Send a message to the server/broker.
* Used for message-driven communication style between microservices.
* @param pattern Pattern to identify the message
* @param data Data to be sent
* @returns Observable with the result
*/
public send<TResult = any, TInput = any>(
pattern: any,
data: TInput,
@@ -101,13 +58,6 @@ export abstract class ClientProxy<
);
}
/**
* Emits an event to the server/broker.
* Used for event-driven communication style between microservices.
* @param pattern Pattern to identify the event
* @param data Data to be sent
* @returns Observable that completes when the event is successfully emitted
*/
public emit<TResult = any, TInput = any>(
pattern: any,
data: TInput,
@@ -164,8 +114,8 @@ export abstract class ClientProxy<
protected connect$(
instance: any,
errorEvent = 'error',
connectEvent = 'connect',
errorEvent = ERROR_EVENT,
connectEvent = CONNECT_EVENT,
): Observable<any> {
const error$ = fromEvent(instance, errorEvent).pipe(
map((err: any) => {

View File

@@ -1,19 +1,14 @@
import { Logger } from '@nestjs/common/services/logger.service';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { REDIS_DEFAULT_HOST, REDIS_DEFAULT_PORT } from '../constants';
import {
RedisEvents,
RedisEventsMap,
RedisStatus,
} from '../events/redis.events';
ERROR_EVENT,
MESSAGE_EVENT,
REDIS_DEFAULT_HOST,
REDIS_DEFAULT_PORT,
} from '../constants';
import { ReadPacket, RedisOptions, WritePacket } from '../interfaces';
import { ClientProxy } from './client-proxy';
// To enable type safety for Redis. This cant be uncommented by default
// because it would require the user to install the ioredis package even if they dont use Redis
// Otherwise, TypeScript would fail to compile the code.
//
// type Redis = import('ioredis').Redis;
type Redis = any;
let redisPackage = {} as any;
@@ -21,18 +16,13 @@ let redisPackage = {} as any;
/**
* @publicApi
*/
export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
export class ClientRedis extends ClientProxy {
protected readonly logger = new Logger(ClientProxy.name);
protected readonly subscriptionsCount = new Map<string, number>();
protected pubClient: Redis;
protected subClient: Redis;
protected connectionPromise: Promise<any>;
protected isManuallyClosed = false;
protected wasInitialConnectionSuccessful = false;
protected pendingEventListeners: Array<{
event: keyof RedisEvents;
callback: RedisEvents[keyof RedisEvents];
}> = [];
protected connection: Promise<any>;
protected isExplicitlyTerminated = false;
constructor(protected readonly options: RedisOptions['options']) {
super();
@@ -57,35 +47,26 @@ export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
this.pubClient && this.pubClient.quit();
this.subClient && this.subClient.quit();
this.pubClient = this.subClient = null;
this.isManuallyClosed = true;
this.pendingEventListeners = [];
this.isExplicitlyTerminated = true;
}
public async connect(): Promise<any> {
if (this.pubClient && this.subClient) {
return this.connectionPromise;
return this.connection;
}
this.pubClient = this.createClient();
this.subClient = this.createClient();
this.handleError(this.pubClient);
this.handleError(this.subClient);
[this.pubClient, this.subClient].forEach((client, index) => {
const type = index === 0 ? 'pub' : 'sub';
this.registerErrorListener(client);
this.registerReconnectListener(client);
this.registerReadyListener(client);
this.registerEndListener(client);
this.pendingEventListeners.forEach(({ event, callback }) =>
client.on(event, (...args: [any]) => callback(type, ...args)),
);
});
this.pendingEventListeners = [];
this.connectionPromise = Promise.all([
this.connection = Promise.all([
this.subClient.connect(),
this.pubClient.connect(),
]);
await this.connectionPromise;
return this.connectionPromise;
await this.connection;
this.subClient.on(MESSAGE_EVENT, this.createResponseCallback());
return this.connection;
}
public createClient(): Redis {
@@ -97,76 +78,8 @@ export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
});
}
public registerErrorListener(client: Redis) {
client.addListener(RedisEventsMap.ERROR, (err: any) =>
this.logger.error(err),
);
}
public registerReconnectListener(client: {
on: (event: string, fn: () => void) => void;
}) {
client.on(RedisEventsMap.RECONNECTING, () => {
if (this.isManuallyClosed) {
return;
}
this.connectionPromise = Promise.reject(
'Error: Connection lost. Trying to reconnect...',
);
// Prevent unhandled rejections
this.connectionPromise.catch(() => {});
this._status$.next(RedisStatus.RECONNECTING);
if (this.wasInitialConnectionSuccessful) {
this.logger.log('Reconnecting to Redis...');
}
});
}
public registerReadyListener(client: {
on: (event: string, fn: () => void) => void;
}) {
client.on(RedisEventsMap.READY, () => {
this.connectionPromise = Promise.resolve();
this._status$.next(RedisStatus.CONNECTED);
this.logger.log('Connected to Redis. Subscribing to channels...');
if (!this.wasInitialConnectionSuccessful) {
this.wasInitialConnectionSuccessful = true;
this.subClient.on('message', this.createResponseCallback());
}
});
}
public registerEndListener(client: {
on: (event: string, fn: () => void) => void;
}) {
client.on('end', () => {
if (this.isManuallyClosed) {
return;
}
this._status$.next(RedisStatus.DISCONNECTED);
if (this.getOptionsProp(this.options, 'retryAttempts') === undefined) {
// When retryAttempts is not specified, the connection will not be re-established
this.logger.error('Disconnected from Redis.');
// Clean up client instances and just recreate them when connect is called
this.pubClient = this.subClient = null;
} else {
this.logger.error('Disconnected from Redis.');
this.connectionPromise = Promise.reject(
'Error: Connection lost. Trying to reconnect...',
);
// Prevent unhandled rejections
this.connectionPromise.catch(() => {});
}
});
public handleError(client: Redis) {
client.addListener(ERROR_EVENT, (err: any) => this.logger.error(err));
}
public getClientOptions(): Partial<RedisOptions['options']> {
@@ -178,42 +91,18 @@ export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
};
}
public on<
EventKey extends keyof RedisEvents = keyof RedisEvents,
EventCallback extends RedisEvents[EventKey] = RedisEvents[EventKey],
>(event: EventKey, callback: EventCallback) {
if (this.subClient && this.pubClient) {
this.subClient.on(event, (...args: [any]) => callback('sub', ...args));
this.pubClient.on(event, (...args: [any]) => callback('pub', ...args));
} else {
this.pendingEventListeners.push({ event, callback });
}
}
public unwrap<T>(): T {
if (!this.pubClient || !this.subClient) {
throw new Error(
'Not initialized. Please call the "connect" method first.',
);
}
return [this.pubClient, this.subClient] as T;
}
public createRetryStrategy(times: number): undefined | number {
if (this.isManuallyClosed) {
if (this.isExplicitlyTerminated) {
return undefined;
}
if (!this.getOptionsProp(this.options, 'retryAttempts')) {
this.logger.error(
'Redis connection closed and retry attempts not specified',
);
return;
}
if (times > this.getOptionsProp(this.options, 'retryAttempts')) {
if (
!this.getOptionsProp(this.options, 'retryAttempts') ||
times > this.getOptionsProp(this.options, 'retryAttempts')
) {
this.logger.error('Retry time exhausted');
return;
}
return this.getOptionsProp(this.options, 'retryDelay') ?? 5000;
return this.getOptionsProp(this.options, 'retryDelay') || 0;
}
public createResponseCallback(): (

View File

@@ -13,7 +13,11 @@ import {
} from 'rxjs';
import { first, map, retryWhen, scan, skip, switchMap } from 'rxjs/operators';
import {
CONNECT_EVENT,
CONNECT_FAILED_EVENT,
DISCONNECT_EVENT,
DISCONNECTED_RMQ_MESSAGE,
ERROR_EVENT,
RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT,
RQM_DEFAULT_NO_ASSERT,
RQM_DEFAULT_NOACK,
@@ -23,53 +27,47 @@ import {
RQM_DEFAULT_QUEUE_OPTIONS,
RQM_DEFAULT_URL,
} from '../constants';
import { RmqEvents, RmqEventsMap, RmqStatus } from '../events/rmq.events';
import { RmqUrl } from '../external/rmq-url.interface';
import { ReadPacket, RmqOptions, WritePacket } from '../interfaces';
import { RmqRecord } from '../record-builders';
import { RmqRecordSerializer } from '../serializers/rmq-record.serializer';
import { ClientProxy } from './client-proxy';
// To enable type safety for RMQ. This cant be uncommented by default
// because it would require the user to install the amqplib package even if they dont use RabbitMQ
// Otherwise, TypeScript would fail to compile the code.
//
// type AmqpConnectionManager =
// import('amqp-connection-manager').AmqpConnectionManager;
// type ChannelWrapper = import('amqp-connection-manager').ChannelWrapper;
// type Channel = import('amqplib').Channel;
// type ConsumeMessage = import('amqplib').ConsumeMessage;
// import type {
// AmqpConnectionManager,
// ChannelWrapper,
// } from 'amqp-connection-manager';
// import type { Channel, ConsumeMessage } from 'amqplib';
type Channel = any;
type ChannelWrapper = any;
type ConsumeMessage = any;
type AmqpConnectionManager = any;
let rmqPackage = {} as any; // typeof import('amqp-connection-manager');
let rmqPackage: any = {};
const REPLY_QUEUE = 'amq.rabbitmq.reply-to';
/**
* @publicApi
*/
export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
export class ClientRMQ extends ClientProxy {
protected readonly logger = new Logger(ClientProxy.name);
protected connection$: ReplaySubject<any>;
protected connectionPromise: Promise<void>;
protected connection: Promise<any>;
protected client: AmqpConnectionManager = null;
protected channel: ChannelWrapper = null;
protected pendingEventListeners: Array<{
event: keyof RmqEvents;
callback: RmqEvents[keyof RmqEvents];
}> = [];
protected isInitialConnect = true;
protected responseEmitter: EventEmitter;
protected urls: string[] | RmqUrl[];
protected queue: string;
protected queueOptions: Record<string, any>;
protected responseEmitter: EventEmitter;
protected replyQueue: string;
protected persistent: boolean;
protected noAssert: boolean;
constructor(protected readonly options: RmqOptions['options']) {
super();
this.urls = this.getOptionsProp(this.options, 'urls') || [RQM_DEFAULT_URL];
this.queue =
this.getOptionsProp(this.options, 'queue') || RQM_DEFAULT_QUEUE;
this.queueOptions =
@@ -77,6 +75,8 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
RQM_DEFAULT_QUEUE_OPTIONS;
this.replyQueue =
this.getOptionsProp(this.options, 'replyQueue') || REPLY_QUEUE;
this.persistent =
this.getOptionsProp(this.options, 'persistent') || RQM_DEFAULT_PERSISTENT;
this.noAssert =
this.getOptionsProp(this.options, 'noAssert') ??
this.queueOptions.noAssert ??
@@ -96,22 +96,15 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
this.client && this.client.close();
this.channel = null;
this.client = null;
this.pendingEventListeners = [];
}
public connect(): Promise<any> {
if (this.client) {
return this.connectionPromise;
return this.convertConnectionToPromise();
}
this.client = this.createClient();
this.registerErrorListener(this.client);
this.registerDisconnectListener(this.client);
this.registerConnectListener(this.client);
this.pendingEventListeners.forEach(({ event, callback }) =>
this.client.on(event, callback),
);
this.pendingEventListeners = [];
this.handleError(this.client);
this.handleDisconnectError(this.client);
this.responseEmitter = new EventEmitter();
this.responseEmitter.setMaxListeners(0);
@@ -122,16 +115,13 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
connect$,
).pipe(switchMap(() => this.createChannel()));
const withReconnect$ = fromEvent(this.client, RmqEventsMap.CONNECT).pipe(
skip(1),
);
const withReconnect$ = fromEvent(this.client, CONNECT_EVENT).pipe(skip(1));
const source$ = merge(withDisconnect$, withReconnect$);
this.connection$ = new ReplaySubject(1);
source$.subscribe(this.connection$);
this.connectionPromise = this.convertConnectionToPromise();
return this.connectionPromise;
return this.convertConnectionToPromise();
}
public createChannel(): Promise<void> {
@@ -145,8 +135,7 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
public createClient(): AmqpConnectionManager {
const socketOptions = this.getOptionsProp(this.options, 'socketOptions');
const urls = this.getOptionsProp(this.options, 'urls') || [RQM_DEFAULT_URL];
return rmqPackage.connect(urls, socketOptions);
return rmqPackage.connect(this.urls, socketOptions);
}
public mergeDisconnectEvent<T = any>(
@@ -159,11 +148,10 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
throw err;
}),
);
const disconnect$ = eventToError(RmqEventsMap.DISCONNECT);
const disconnect$ = eventToError(DISCONNECT_EVENT);
const urls = this.getOptionsProp(this.options, 'urls', []);
const connectFailedEventKey = 'connectFailed';
const connectFailed$ = eventToError(connectFailedEventKey).pipe(
const connectFailed$ = eventToError(CONNECT_FAILED_EVENT).pipe(
retryWhen(e =>
e.pipe(
scan((errorCount, error: any) => {
@@ -202,15 +190,6 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
if (!this.noAssert) {
await channel.assertQueue(this.queue, this.queueOptions);
}
if (this.options.exchange && this.options.routingKey) {
await channel.bindQueue(
this.queue,
this.options.exchange,
this.options.routingKey,
);
}
await channel.prefetch(prefetchCount, isGlobalPrefetchCount);
await this.consumeChannel(channel);
resolve();
@@ -228,81 +207,31 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
);
}
public registerErrorListener(client: AmqpConnectionManager): void {
client.addListener(RmqEventsMap.ERROR, (err: any) =>
this.logger.error(err),
);
public handleError(client: AmqpConnectionManager): void {
client.addListener(ERROR_EVENT, (err: any) => this.logger.error(err));
}
public registerDisconnectListener(client: AmqpConnectionManager): void {
client.addListener(RmqEventsMap.DISCONNECT, (err: any) => {
this._status$.next(RmqStatus.DISCONNECTED);
if (!this.isInitialConnect) {
this.connectionPromise = Promise.reject(
'Error: Connection lost. Trying to reconnect...',
);
// Prevent unhandled promise rejection
this.connectionPromise.catch(() => {});
}
public handleDisconnectError(client: AmqpConnectionManager): void {
client.addListener(DISCONNECT_EVENT, (err: any) => {
this.logger.error(DISCONNECTED_RMQ_MESSAGE);
this.logger.error(err);
});
}
private registerConnectListener(client: AmqpConnectionManager): void {
client.addListener(RmqEventsMap.CONNECT, () => {
this._status$.next(RmqStatus.CONNECTED);
this.logger.log('Successfully connected to RMQ broker');
if (this.isInitialConnect) {
this.isInitialConnect = false;
if (!this.channel) {
this.connectionPromise = this.createChannel();
}
} else {
this.connectionPromise = Promise.resolve();
}
});
}
public on<
EventKey extends keyof RmqEvents = keyof RmqEvents,
EventCallback extends RmqEvents[EventKey] = RmqEvents[EventKey],
>(event: EventKey, callback: EventCallback) {
if (this.client) {
this.client.addListener(event, callback);
} else {
this.pendingEventListeners.push({ event, callback });
}
}
public unwrap<T>(): T {
if (!this.client) {
throw new Error(
'Not initialized. Please call the "connect" method first.',
);
}
return this.client as T;
}
public async handleMessage(
packet: unknown,
callback: (packet: WritePacket) => any,
): Promise<void>;
);
public async handleMessage(
packet: unknown,
options: Record<string, unknown>,
callback: (packet: WritePacket) => any,
): Promise<void>;
);
public async handleMessage(
packet: unknown,
options: Record<string, unknown> | ((packet: WritePacket) => any),
callback?: (packet: WritePacket) => any,
): Promise<void> {
) {
if (isFunction(options)) {
callback = options as (packet: WritePacket) => any;
options = undefined;
@@ -358,11 +287,7 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
Buffer.from(JSON.stringify(serializedPacket)),
{
replyTo: this.replyQueue,
persistent: this.getOptionsProp(
this.options,
'persistent',
RQM_DEFAULT_PERSISTENT,
),
persistent: this.persistent,
...options,
headers: this.mergeHeaders(options?.headers),
correlationId,
@@ -387,11 +312,7 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
this.queue,
Buffer.from(JSON.stringify(serializedPacket)),
{
persistent: this.getOptionsProp(
this.options,
'persistent',
RQM_DEFAULT_PERSISTENT,
),
persistent: this.persistent,
...options,
headers: this.mergeHeaders(options?.headers),
},

View File

@@ -2,10 +2,17 @@ import { Logger, Type } from '@nestjs/common';
import * as net from 'net';
import { EmptyError, lastValueFrom } from 'rxjs';
import { share, tap } from 'rxjs/operators';
import { ConnectionOptions, connect as tlsConnect, TLSSocket } from 'tls';
import { ECONNREFUSED, TCP_DEFAULT_HOST, TCP_DEFAULT_PORT } from '../constants';
import { TcpEvents, TcpEventsMap, TcpStatus } from '../events/tcp.events';
import { ConnectionOptions } from 'tls';
import {
CLOSE_EVENT,
ECONNREFUSED,
ERROR_EVENT,
MESSAGE_EVENT,
TCP_DEFAULT_HOST,
TCP_DEFAULT_PORT,
} from '../constants';
import { JsonSocket, TcpSocket } from '../helpers';
import { connect as tlsConnect, TLSSocket } from 'tls';
import { PacketId, ReadPacket, WritePacket } from '../interfaces';
import { TcpClientOptions } from '../interfaces/client-metadata.interface';
import { ClientProxy } from './client-proxy';
@@ -13,18 +20,15 @@ import { ClientProxy } from './client-proxy';
/**
* @publicApi
*/
export class ClientTCP extends ClientProxy<TcpEvents, TcpStatus> {
protected readonly logger = new Logger(ClientTCP.name);
protected readonly port: number;
protected readonly host: string;
protected readonly socketClass: Type<TcpSocket>;
protected readonly tlsOptions?: ConnectionOptions;
protected socket: TcpSocket;
protected connectionPromise: Promise<any>;
protected pendingEventListeners: Array<{
event: keyof TcpEvents;
callback: TcpEvents[keyof TcpEvents];
}> = [];
export class ClientTCP extends ClientProxy {
protected connection: Promise<any>;
private readonly logger = new Logger(ClientTCP.name);
private readonly port: number;
private readonly host: string;
private readonly socketClass: Type<TcpSocket>;
private isConnected = false;
private socket: TcpSocket;
public tlsOptions?: ConnectionOptions;
constructor(options: TcpClientOptions['options']) {
super();
@@ -39,22 +43,16 @@ export class ClientTCP extends ClientProxy<TcpEvents, TcpStatus> {
}
public connect(): Promise<any> {
if (this.connectionPromise) {
return this.connectionPromise;
if (this.connection) {
return this.connection;
}
this.socket = this.createSocket();
this.registerConnectListener(this.socket);
this.registerCloseListener(this.socket);
this.registerErrorListener(this.socket);
this.pendingEventListeners.forEach(({ event, callback }) =>
this.socket.on(event, callback as any),
);
this.pendingEventListeners = [];
this.bindEvents(this.socket);
const source$ = this.connect$(this.socket.netSocket).pipe(
tap(() => {
this.socket.on('message', (buffer: WritePacket & PacketId) =>
this.isConnected = true;
this.socket.on(MESSAGE_EVENT, (buffer: WritePacket & PacketId) =>
this.handleResponse(buffer),
);
}),
@@ -65,14 +63,14 @@ export class ClientTCP extends ClientProxy<TcpEvents, TcpStatus> {
if (!this.tlsOptions) {
this.socket.connect(this.port, this.host);
}
this.connectionPromise = lastValueFrom(source$).catch(err => {
this.connection = lastValueFrom(source$).catch(err => {
if (err instanceof EmptyError) {
return;
}
throw err;
});
return this.connectionPromise;
return this.connection;
}
public async handleResponse(buffer: unknown): Promise<void> {
@@ -116,30 +114,14 @@ export class ClientTCP extends ClientProxy<TcpEvents, TcpStatus> {
public close() {
this.socket && this.socket.end();
this.handleClose();
this.pendingEventListeners = [];
}
public registerConnectListener(socket: TcpSocket) {
socket.on(TcpEventsMap.CONNECT, () => {
this._status$.next(TcpStatus.CONNECTED);
});
}
public registerErrorListener(socket: TcpSocket) {
socket.on(TcpEventsMap.ERROR, err => {
if (err.code !== ECONNREFUSED) {
this.handleError(err);
} else {
this._status$.next(TcpStatus.DISCONNECTED);
}
});
}
public registerCloseListener(socket: TcpSocket) {
socket.on(TcpEventsMap.CLOSE, () => {
this._status$.next(TcpStatus.DISCONNECTED);
this.handleClose();
});
public bindEvents(socket: TcpSocket) {
socket.on(
ERROR_EVENT,
(err: any) => err.code !== ECONNREFUSED && this.handleError(err),
);
socket.on(CLOSE_EVENT, () => this.handleClose());
}
public handleError(err: any) {
@@ -147,8 +129,9 @@ export class ClientTCP extends ClientProxy<TcpEvents, TcpStatus> {
}
public handleClose() {
this.isConnected = false;
this.socket = null;
this.connectionPromise = undefined;
this.connection = undefined;
if (this.routingMap.size > 0) {
const err = new Error('Connection closed');
@@ -159,26 +142,6 @@ export class ClientTCP extends ClientProxy<TcpEvents, TcpStatus> {
}
}
public on<
EventKey extends keyof TcpEvents = keyof TcpEvents,
EventCallback extends TcpEvents[EventKey] = TcpEvents[EventKey],
>(event: EventKey, callback: EventCallback) {
if (this.socket) {
this.socket.on(event, callback as any);
} else {
this.pendingEventListeners.push({ event, callback });
}
}
public unwrap<T>(): T {
if (!this.socket) {
throw new Error(
'Not initialized. Please call the "connect" method first.',
);
}
return this.socket.netSocket as T;
}
protected publish(
partialPacket: ReadPacket,
callback: (packet: WritePacket) => any,

View File

@@ -0,0 +1,2 @@
export const GRPC_CANCELLED = 'Cancelled';
export const RABBITMQ_REPLY_QUEUE = 'amq.rabbitmq.reply-to';

View File

@@ -2,29 +2,25 @@ import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
export const TCP_DEFAULT_PORT = 3000;
export const TCP_DEFAULT_HOST = 'localhost';
export const REDIS_DEFAULT_PORT = 6379;
export const REDIS_DEFAULT_HOST = 'localhost';
export const NATS_DEFAULT_URL = 'nats://localhost:4222';
export const MQTT_DEFAULT_URL = 'mqtt://localhost:1883';
export const GRPC_DEFAULT_URL = 'localhost:5000';
export const RQM_DEFAULT_URL = 'amqp://localhost';
export const KAFKA_DEFAULT_BROKER = 'localhost:9092';
export const KAFKA_DEFAULT_CLIENT = 'nestjs-consumer';
export const KAFKA_DEFAULT_GROUP = 'nestjs-group';
export const MQTT_SEPARATOR = '/';
export const MQTT_WILDCARD_SINGLE = '+';
export const MQTT_WILDCARD_ALL = '#';
export const RQM_DEFAULT_QUEUE = 'default';
export const RQM_DEFAULT_PREFETCH_COUNT = 0;
export const RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT = false;
export const RQM_DEFAULT_QUEUE_OPTIONS = {};
export const RQM_DEFAULT_NOACK = true;
export const RQM_DEFAULT_PERSISTENT = false;
export const RQM_DEFAULT_NO_ASSERT = false;
export const ECONNREFUSED = 'ECONNREFUSED';
export const CONN_ERR = 'CONN_ERR';
export const EADDRINUSE = 'EADDRINUSE';
export const CONNECT_EVENT = 'connect';
export const DISCONNECT_EVENT = 'disconnect';
export const CONNECT_FAILED_EVENT = 'connectFailed';
export const MESSAGE_EVENT = 'message';
export const DATA_EVENT = 'data';
export const ERROR_EVENT = 'error';
export const CLOSE_EVENT = 'close';
export const SUBSCRIBE = 'subscribe';
export const CANCEL_EVENT = 'cancelled';
export const PATTERN_METADATA = 'microservices:pattern';
export const PATTERN_EXTRAS_METADATA = 'microservices:pattern_extras';
@@ -33,9 +29,17 @@ export const CLIENT_CONFIGURATION_METADATA = 'microservices:client';
export const PATTERN_HANDLER_METADATA = 'microservices:handler_type';
export const CLIENT_METADATA = 'microservices:is_client_instance';
export const PARAM_ARGS_METADATA = ROUTE_ARGS_METADATA;
export const REQUEST_PATTERN_METADATA = 'microservices:request_pattern';
export const REPLY_PATTERN_METADATA = 'microservices:reply_pattern';
export const RQM_DEFAULT_QUEUE = 'default';
export const RQM_DEFAULT_PREFETCH_COUNT = 0;
export const RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT = false;
export const RQM_DEFAULT_QUEUE_OPTIONS = {};
export const RQM_DEFAULT_NOACK = true;
export const RQM_DEFAULT_PERSISTENT = false;
export const RQM_DEFAULT_NO_ASSERT = false;
export const RQM_NO_EVENT_HANDLER = (
text: TemplateStringsArray,
pattern: string,
@@ -51,8 +55,19 @@ export const GRPC_DEFAULT_PROTO_LOADER = '@grpc/proto-loader';
export const NO_EVENT_HANDLER = (text: TemplateStringsArray, pattern: string) =>
`There is no matching event handler defined in the remote service. Event pattern: ${pattern}`;
export const NO_MESSAGE_HANDLER = `There is no matching message handler defined in the remote service.`;
export const DISCONNECTED_RMQ_MESSAGE = `Disconnected from RMQ. Trying to reconnect.`;
export const KAFKA_DEFAULT_CLIENT = 'nestjs-consumer';
export const KAFKA_DEFAULT_GROUP = 'nestjs-group';
export const MQTT_SEPARATOR = '/';
export const MQTT_WILDCARD_SINGLE = '+';
export const MQTT_WILDCARD_ALL = '#';
export const ECONNREFUSED = 'ECONNREFUSED';
export const CONN_ERR = 'CONN_ERR';
export const EADDRINUSE = 'EADDRINUSE';
export const CONNECTION_FAILED_MESSAGE =
'Connection to transport failed. Trying to reconnect...';
export const NATS_DEFAULT_GRACE_PERIOD = 10000;

View File

@@ -1,13 +1,16 @@
import { ClientProxy } from './client/client-proxy';
import { Closeable } from './interfaces/closeable.interface';
export type CloseableClient = Closeable & ClientProxy;
export class ClientsContainer {
private clients: ClientProxy[] = [];
private clients: CloseableClient[] = [];
public getAllClients(): ClientProxy[] {
public getAllClients(): CloseableClient[] {
return this.clients;
}
public addClient(client: ClientProxy) {
public addClient(client: CloseableClient) {
this.clients.push(client);
}

View File

@@ -1,23 +1,23 @@
import {
isObject,
isNumber,
isNil,
isNumber,
isObject,
isSymbol,
} from '@nestjs/common/utils/shared.utils';
/* eslint-disable @typescript-eslint/no-use-before-define */
import {
PATTERN_EXTRAS_METADATA,
PATTERN_HANDLER_METADATA,
PATTERN_METADATA,
TRANSPORT_METADATA,
PATTERN_EXTRAS_METADATA,
} from '../constants';
import { PatternHandler } from '../enums/pattern-handler.enum';
import { PatternMetadata } from '../interfaces/pattern-metadata.interface';
import { Transport } from '../enums';
import { PatternHandler } from '../enums/pattern-handler.enum';
import {
InvalidGrpcDecoratorException,
RpcDecoratorMetadata,
} from '../errors/invalid-grpc-message-decorator.exception';
import { PatternMetadata } from '../interfaces/pattern-metadata.interface';
export enum GrpcMethodStreamingType {
NO_STREAMING = 'no_stream',
@@ -141,7 +141,37 @@ export function GrpcStreamMethod(
method,
GrpcMethodStreamingType.RX_STREAMING,
);
return MessagePattern(metadata, Transport.GRPC)(target, key, descriptor);
MessagePattern(metadata, Transport.GRPC)(target, key, descriptor);
const originalMethod = descriptor.value;
// Override original method to call the "drainBuffer" method on the first parameter
// This is required to avoid premature message emission
descriptor.value = async function (
this: any,
observable: any,
...args: any[]
) {
const result = await Promise.resolve(
originalMethod.apply(this, [observable, ...args]),
);
// Drain buffer if "drainBuffer" method is available
if (observable && observable.drainBuffer) {
process.nextTick(() => {
observable.drainBuffer();
});
}
return result;
};
// Copy all metadata from the original method to the new one
const metadataKeys = Reflect.getMetadataKeys(originalMethod);
metadataKeys.forEach(metadataKey => {
const metadataValue = Reflect.getMetadata(metadataKey, originalMethod);
Reflect.defineMetadata(metadataKey, metadataValue, descriptor.value);
});
};
}

View File

@@ -1,5 +1,5 @@
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { NatsCodec } from '../external/nats-codec.interface';
import { NatsCodec } from '../external/nats-client.interface';
import { IncomingEvent, IncomingRequest } from '../interfaces';
import { IncomingRequestDeserializer } from './incoming-request.deserializer';

View File

@@ -1,5 +1,5 @@
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { NatsCodec } from '../external/nats-codec.interface';
import { NatsCodec } from '../external/nats-client.interface';
import { IncomingResponse } from '../interfaces';
import { IncomingResponseDeserializer } from './incoming-response.deserializer';
import { NatsRequestJSONDeserializer } from './nats-request-json.deserializer';

View File

@@ -1,8 +0,0 @@
/**
* @publicApi
*/
export class MaxPacketLengthExceededException extends Error {
constructor(length: number) {
super(`The packet length (${length}) exceeds the maximum allowed length`);
}
}

Some files were not shown because too many files have changed in this diff Show More