mirror of
https://github.com/nestjs/nest.git
synced 2026-02-23 07:42:52 +00:00
Compare commits
537 Commits
miZyind-co
...
v7.6.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a96b08465e | ||
|
|
c47c45b6ff | ||
|
|
93b5598a33 | ||
|
|
29497d61af | ||
|
|
f37e23c5ea | ||
|
|
fa0e011b03 | ||
|
|
d034e62ff0 | ||
|
|
396fe779e6 | ||
|
|
5728578e10 | ||
|
|
0db1b041ef | ||
|
|
821574cee9 | ||
|
|
90d64d085d | ||
|
|
6db30813de | ||
|
|
b336fd172c | ||
|
|
3123ad3f19 | ||
|
|
fc5f3c91af | ||
|
|
e51ab047d2 | ||
|
|
2ba2b99325 | ||
|
|
ba37eee5b0 | ||
|
|
4804f32472 | ||
|
|
40a0678fbf | ||
|
|
a11d167130 | ||
|
|
6b119c3579 | ||
|
|
f22d3370f3 | ||
|
|
c12100c3c6 | ||
|
|
8cf29c9172 | ||
|
|
12ac1108a7 | ||
|
|
c150deefbd | ||
|
|
68b5d2a8e2 | ||
|
|
4d87dd6b1e | ||
|
|
e0b825121b | ||
|
|
dc8b1d5c72 | ||
|
|
832b3bc90c | ||
|
|
8180e5905c | ||
|
|
2307373bfe | ||
|
|
71255392c9 | ||
|
|
07c8cdc80b | ||
|
|
f9e5272e2b | ||
|
|
b842a7e817 | ||
|
|
8df890a8ac | ||
|
|
060befe22e | ||
|
|
06bddccb5c | ||
|
|
4f88c3d147 | ||
|
|
dd7c176df3 | ||
|
|
c43fb62322 | ||
|
|
8c4fb929b7 | ||
|
|
1130edc150 | ||
|
|
6417c5477a | ||
|
|
149483a852 | ||
|
|
27d9202108 | ||
|
|
950e3697c7 | ||
|
|
df5a7c3a48 | ||
|
|
25b7ad5805 | ||
|
|
0f668075fb | ||
|
|
b74f80fb1c | ||
|
|
a0d08f049c | ||
|
|
c88db600a0 | ||
|
|
e196f51608 | ||
|
|
f4c45f8d88 | ||
|
|
4baea48cfa | ||
|
|
275e71f89a | ||
|
|
4a1074a793 | ||
|
|
e9d1735688 | ||
|
|
3b93861706 | ||
|
|
6d1c02be01 | ||
|
|
1e5f60ed17 | ||
|
|
6379162871 | ||
|
|
fddc980b1d | ||
|
|
6027001b4e | ||
|
|
0cd83570a2 | ||
|
|
48d91c9646 | ||
|
|
9b9b3564a4 | ||
|
|
25526070a1 | ||
|
|
6df1712b0d | ||
|
|
8aa0f75c8c | ||
|
|
c0babd6cfd | ||
|
|
6448d17f43 | ||
|
|
6a9c9100f5 | ||
|
|
55c7051787 | ||
|
|
db6a555d29 | ||
|
|
7c722c3d0b | ||
|
|
e7011e3b4f | ||
|
|
0fb8e1a025 | ||
|
|
459458bbb4 | ||
|
|
9e96e76e1a | ||
|
|
a63db1b8aa | ||
|
|
477b6d0d5c | ||
|
|
90aa625bf8 | ||
|
|
e8541185f0 | ||
|
|
c48e2bcfdb | ||
|
|
667df81eb5 | ||
|
|
fb5117c241 | ||
|
|
ec9727093d | ||
|
|
d2e6616115 | ||
|
|
9f3614042f | ||
|
|
2bc2c8adfa | ||
|
|
e4c5d17df2 | ||
|
|
bf420cd91d | ||
|
|
5d48691713 | ||
|
|
454d519c67 | ||
|
|
60f256ecdf | ||
|
|
df4c2eb318 | ||
|
|
843116eea9 | ||
|
|
3af5404401 | ||
|
|
9a6610cb07 | ||
|
|
a76a6e4a30 | ||
|
|
d063c4772d | ||
|
|
64d78a9c80 | ||
|
|
efe4884c27 | ||
|
|
5ebd3353b1 | ||
|
|
5d2fa36556 | ||
|
|
1440ccaa4b | ||
|
|
42262df197 | ||
|
|
6f119c843c | ||
|
|
cb0564d77a | ||
|
|
d28c25618d | ||
|
|
4a57915f5a | ||
|
|
3188b801bd | ||
|
|
b7e46f45d4 | ||
|
|
993e5f4a91 | ||
|
|
bc394eabb2 | ||
|
|
0b874e7dfc | ||
|
|
a65fcd1472 | ||
|
|
159cdce1f1 | ||
|
|
b298b8bc92 | ||
|
|
53226211f7 | ||
|
|
d4bb87c5eb | ||
|
|
fa4642f70f | ||
|
|
c4c1b7c6a5 | ||
|
|
2be4c4cd70 | ||
|
|
76289893d7 | ||
|
|
b3e654e84d | ||
|
|
668d781b6c | ||
|
|
fb45437621 | ||
|
|
0c7bf92086 | ||
|
|
091d09fde2 | ||
|
|
653f0a4281 | ||
|
|
48d899d025 | ||
|
|
81f3161f7c | ||
|
|
0f29446c65 | ||
|
|
1b31a744a8 | ||
|
|
0fe31b9bc8 | ||
|
|
5da5a4ad58 | ||
|
|
2abb9b2154 | ||
|
|
43f053ab21 | ||
|
|
a74725d585 | ||
|
|
51f5de8333 | ||
|
|
0c8b004bf9 | ||
|
|
3ce92c7883 | ||
|
|
3bbde4c26c | ||
|
|
a48107ef92 | ||
|
|
b7990aa5b1 | ||
|
|
07737aa371 | ||
|
|
ca7b79532f | ||
|
|
ad6ce62f0e | ||
|
|
4c814a2d85 | ||
|
|
1dbb8ff3db | ||
|
|
c6780a3a91 | ||
|
|
1945f8d24a | ||
|
|
ccd4fdae58 | ||
|
|
817d9f45e0 | ||
|
|
9362db8042 | ||
|
|
2fb5f4dd16 | ||
|
|
a400be366b | ||
|
|
7267409d91 | ||
|
|
fa494041c8 | ||
|
|
af4f041d6a | ||
|
|
562b2bb175 | ||
|
|
36c2a35614 | ||
|
|
fb4cc36c75 | ||
|
|
7386204da6 | ||
|
|
d99370bb1d | ||
|
|
90fcd3599e | ||
|
|
e3a66d1902 | ||
|
|
71cab143e6 | ||
|
|
3357fe0349 | ||
|
|
be0cdfe7e6 | ||
|
|
c9c92b47de | ||
|
|
6fb25e93e2 | ||
|
|
6957b52afe | ||
|
|
5b7eae83e0 | ||
|
|
52546cc62e | ||
|
|
d43c4d7510 | ||
|
|
b4c24cc17e | ||
|
|
119190ad75 | ||
|
|
3361cfd236 | ||
|
|
0cf4409869 | ||
|
|
5028393d57 | ||
|
|
05b771a9c2 | ||
|
|
d972d9ad8f | ||
|
|
668968faa9 | ||
|
|
d588b8bc68 | ||
|
|
1a0922a58b | ||
|
|
cd04a8d58a | ||
|
|
ff935dac06 | ||
|
|
ba2f5ae63a | ||
|
|
0d67f823d5 | ||
|
|
f001a9ab83 | ||
|
|
8bc4e92d21 | ||
|
|
d2d951870c | ||
|
|
29e158ac66 | ||
|
|
29192b38e4 | ||
|
|
e6e11b9cf1 | ||
|
|
38974dc290 | ||
|
|
8b86d836c3 | ||
|
|
967206ca5d | ||
|
|
ca830a8519 | ||
|
|
ce28bee865 | ||
|
|
9e20b0a858 | ||
|
|
b60691737f | ||
|
|
41415b5b91 | ||
|
|
65d2fac038 | ||
|
|
fe0b7bc95b | ||
|
|
b876a5542c | ||
|
|
b14c7de30b | ||
|
|
439512a748 | ||
|
|
fa0024e581 | ||
|
|
854522eb88 | ||
|
|
3f67e9a0f2 | ||
|
|
b5a92dd1be | ||
|
|
9c15dc692d | ||
|
|
7efeba63c6 | ||
|
|
1db29b3632 | ||
|
|
32d06a2bea | ||
|
|
a04d19b339 | ||
|
|
27e1748ae9 | ||
|
|
025fb6bae9 | ||
|
|
d7fafd59ab | ||
|
|
54514b636b | ||
|
|
b0de6b20c7 | ||
|
|
d13fad341a | ||
|
|
ad955270cf | ||
|
|
7f300dc640 | ||
|
|
06b220fd92 | ||
|
|
d823d9bf75 | ||
|
|
a58db3a89d | ||
|
|
3b8e4172e7 | ||
|
|
6cd9b7e6d4 | ||
|
|
989582529b | ||
|
|
5210db3050 | ||
|
|
ae76f46e3c | ||
|
|
9c102342d9 | ||
|
|
7e79847c61 | ||
|
|
da4fc69e2b | ||
|
|
25b6e501f8 | ||
|
|
1f4f8222b1 | ||
|
|
a85cd23088 | ||
|
|
b0cc4a2ec5 | ||
|
|
e235bcfcd5 | ||
|
|
4841d48e93 | ||
|
|
dac636f0c1 | ||
|
|
dfefa63697 | ||
|
|
30e90b7a3a | ||
|
|
045983d7aa | ||
|
|
660be0ed86 | ||
|
|
c523845855 | ||
|
|
d560888761 | ||
|
|
fdd678c013 | ||
|
|
4950c53586 | ||
|
|
57ebb8e04e | ||
|
|
e77b185828 | ||
|
|
bc74ef28a3 | ||
|
|
9e6667f9e8 | ||
|
|
d74ae63e83 | ||
|
|
a4c475ddc2 | ||
|
|
aa4c8608ed | ||
|
|
13b366878e | ||
|
|
c6bf80f76c | ||
|
|
3080f75534 | ||
|
|
766218aad1 | ||
|
|
d6b2266615 | ||
|
|
344f6a6efc | ||
|
|
9421dae875 | ||
|
|
27dafd8292 | ||
|
|
5150334252 | ||
|
|
6f9ae1c871 | ||
|
|
65c6c443a6 | ||
|
|
732d378c62 | ||
|
|
55033eb6b4 | ||
|
|
5f103779b4 | ||
|
|
2753474871 | ||
|
|
aeec26c25a | ||
|
|
55d7542cb1 | ||
|
|
987faa5f2e | ||
|
|
c5aa2c7b01 | ||
|
|
73059b574f | ||
|
|
98c9674b26 | ||
|
|
a1c76b4a7e | ||
|
|
eabc87063e | ||
|
|
562cd3d6ee | ||
|
|
4b35074d5c | ||
|
|
287b8a4371 | ||
|
|
b627789e7e | ||
|
|
c313e14add | ||
|
|
a0a2aeba77 | ||
|
|
70724f4aed | ||
|
|
fcccd4ccad | ||
|
|
a45039ae45 | ||
|
|
a2c6cb3ef9 | ||
|
|
7b411cb57d | ||
|
|
b75802cadc | ||
|
|
65663d3328 | ||
|
|
93d7962d10 | ||
|
|
e27b236dbe | ||
|
|
62afe6729c | ||
|
|
6551398a9b | ||
|
|
40dbd07fc8 | ||
|
|
be1a75c57f | ||
|
|
d1e6666eeb | ||
|
|
4daefbf020 | ||
|
|
a919dd237b | ||
|
|
c59e955901 | ||
|
|
9a2a8b80bf | ||
|
|
a880fc886e | ||
|
|
8cdfa23815 | ||
|
|
3cfa0c74d2 | ||
|
|
b4938d8721 | ||
|
|
7ef921aca1 | ||
|
|
acd2d26d53 | ||
|
|
37c5d16db3 | ||
|
|
7c93e55ccb | ||
|
|
54186a429f | ||
|
|
0321d77dcd | ||
|
|
66a1430a85 | ||
|
|
861a53c3d3 | ||
|
|
15213d93c3 | ||
|
|
7aedd0cc88 | ||
|
|
bfc7bea887 | ||
|
|
428605e63a | ||
|
|
61022bea9c | ||
|
|
e0df855423 | ||
|
|
9a806d6851 | ||
|
|
28cb30743b | ||
|
|
85dcd8a2fe | ||
|
|
97b9836a58 | ||
|
|
bfdc8f4025 | ||
|
|
f1bc0b709d | ||
|
|
9304c56847 | ||
|
|
ca487ce995 | ||
|
|
4603979d67 | ||
|
|
658e26b274 | ||
|
|
86051702b8 | ||
|
|
c061debf1a | ||
|
|
9be7fc5277 | ||
|
|
1957d75db7 | ||
|
|
82a57093ba | ||
|
|
ad657f9aa5 | ||
|
|
0b7ca5f6f3 | ||
|
|
fdcb89d0d0 | ||
|
|
01d0271b9c | ||
|
|
5f24eebf25 | ||
|
|
2e33120949 | ||
|
|
0ff0c86631 | ||
|
|
a55b27745f | ||
|
|
fcb9cec9c8 | ||
|
|
3b6a8ba1de | ||
|
|
d1d08ac2ef | ||
|
|
c7a4128631 | ||
|
|
e8d6e312bb | ||
|
|
2525e9406d | ||
|
|
b7e916c8de | ||
|
|
d456c73f85 | ||
|
|
dd51cf8f51 | ||
|
|
2ec37298e0 | ||
|
|
e8e2cc2f38 | ||
|
|
b86d25ddf8 | ||
|
|
3a700e2d19 | ||
|
|
95ef3a778c | ||
|
|
bcd4ea93b1 | ||
|
|
0cc464519f | ||
|
|
bacef33be1 | ||
|
|
c624c170dd | ||
|
|
d9c4162bb2 | ||
|
|
fc557353be | ||
|
|
6c800fd2e3 | ||
|
|
cee29d9406 | ||
|
|
7d7cf01813 | ||
|
|
a05ff8b6b0 | ||
|
|
b63cd49f83 | ||
|
|
be6c85c88d | ||
|
|
93acb0af17 | ||
|
|
a80547e4a6 | ||
|
|
16246feddd | ||
|
|
1a43fd05e8 | ||
|
|
24ce1dd6de | ||
|
|
51a2e48a4a | ||
|
|
a3f32a342d | ||
|
|
9f96855a46 | ||
|
|
e2e2a8f746 | ||
|
|
094fc36115 | ||
|
|
854bf58910 | ||
|
|
5cba807a98 | ||
|
|
8df77f8f45 | ||
|
|
a796bfabea | ||
|
|
f6f6f917ad | ||
|
|
f5770bf13b | ||
|
|
1a4530e669 | ||
|
|
2b1529923e | ||
|
|
69e229774c | ||
|
|
82eb97401b | ||
|
|
5134ccd933 | ||
|
|
e23970d748 | ||
|
|
3102db3aa0 | ||
|
|
452ffb4125 | ||
|
|
5a33605808 | ||
|
|
51a5de1287 | ||
|
|
a64116b2bf | ||
|
|
1426d4c860 | ||
|
|
24113d9de9 | ||
|
|
33e2c65892 | ||
|
|
c21a1526ad | ||
|
|
f1d1ab8a03 | ||
|
|
0d902753c9 | ||
|
|
ef172c3e21 | ||
|
|
ae6f24b9fe | ||
|
|
8db37bdbbe | ||
|
|
0b21ccb216 | ||
|
|
a02be476fc | ||
|
|
b2e964a611 | ||
|
|
594ea3a734 | ||
|
|
bcc3fd3881 | ||
|
|
0dd86eb730 | ||
|
|
3dbf5f81f2 | ||
|
|
495de37512 | ||
|
|
9628c2dd76 | ||
|
|
85e50eb8e7 | ||
|
|
44f039820d | ||
|
|
b0e37bb746 | ||
|
|
9d0c96e953 | ||
|
|
a3366e11a5 | ||
|
|
e06be8fb30 | ||
|
|
5da76830fe | ||
|
|
8409a18420 | ||
|
|
413ca1990e | ||
|
|
b10c3cb30e | ||
|
|
61ee778554 | ||
|
|
ceb7ff8628 | ||
|
|
192ae54de0 | ||
|
|
66a1231a0c | ||
|
|
09f5af366a | ||
|
|
d8d71b0c15 | ||
|
|
a8186d8160 | ||
|
|
8d3f139286 | ||
|
|
bbf091c77c | ||
|
|
192d22f5e0 | ||
|
|
2ec1281b17 | ||
|
|
b8a9772d51 | ||
|
|
021c32380b | ||
|
|
72cd8ff732 | ||
|
|
fdb93219ad | ||
|
|
a9451b64fc | ||
|
|
d36c938548 | ||
|
|
63048baf99 | ||
|
|
a52001dfb9 | ||
|
|
bb38257d13 | ||
|
|
703afcb1ba | ||
|
|
80f5a9ce48 | ||
|
|
5a23bc1749 | ||
|
|
bb6b643645 | ||
|
|
710b385a32 | ||
|
|
e78f364ef4 | ||
|
|
23ce8d03e4 | ||
|
|
4c4d8039b8 | ||
|
|
016c6b554b | ||
|
|
3f5aad635b | ||
|
|
1c500b4bfc | ||
|
|
cad68b6d46 | ||
|
|
a22abe53b5 | ||
|
|
65afede473 | ||
|
|
171b53c3fb | ||
|
|
8875d431b4 | ||
|
|
0d078a0188 | ||
|
|
4dd0a1344e | ||
|
|
585ead7f24 | ||
|
|
be64b51796 | ||
|
|
55a2efd883 | ||
|
|
338be4c8d0 | ||
|
|
b0afb1a63a | ||
|
|
59cd40dd56 | ||
|
|
685c685bd7 | ||
|
|
be2384c4c7 | ||
|
|
e7ef9d9c70 | ||
|
|
fc5b440da2 | ||
|
|
998d3de9e6 | ||
|
|
e766276922 | ||
|
|
2abb426d99 | ||
|
|
4fc147f6e0 | ||
|
|
63337763a2 | ||
|
|
ab6960da62 | ||
|
|
a0ded7ad68 | ||
|
|
15aca2bfef | ||
|
|
9df5146082 | ||
|
|
c79685c88b | ||
|
|
9c8cf3978a | ||
|
|
8c9ee77fb8 | ||
|
|
fb3db3f6bf | ||
|
|
8c503d3193 | ||
|
|
b65f1be3f2 | ||
|
|
9a64742f61 | ||
|
|
1e32627d17 | ||
|
|
e4243d55c2 | ||
|
|
684c5f4af7 | ||
|
|
51e3559995 | ||
|
|
beb8fe36fc | ||
|
|
2a4dcc1964 | ||
|
|
e7150ba5a9 | ||
|
|
a0a3fb96cf | ||
|
|
7ef474e74e | ||
|
|
8dc4967721 | ||
|
|
419167ad0f | ||
|
|
35add88251 | ||
|
|
0f80068198 | ||
|
|
32b19c7c37 | ||
|
|
827f1e7f77 | ||
|
|
404a916129 | ||
|
|
1e1a52eb20 | ||
|
|
c67b58f527 | ||
|
|
4763fb9552 | ||
|
|
68ad994f2d | ||
|
|
0f15e35f2b | ||
|
|
25ca219254 | ||
|
|
525ef91307 | ||
|
|
7dad2e479b | ||
|
|
530274e2c8 | ||
|
|
5e21c9e51e | ||
|
|
b0a0623cf8 | ||
|
|
9449d612aa | ||
|
|
15473e4251 | ||
|
|
1a46de0af3 | ||
|
|
453e4afd3e | ||
|
|
ffd602ffd5 | ||
|
|
badae7b4e7 | ||
|
|
d24d0381a2 | ||
|
|
a85cb0fe4b | ||
|
|
cc9a912d0a | ||
|
|
85dcf72508 | ||
|
|
f4df4d9a9e |
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2017-2020 Kamil Mysliwiec <https://kamilmysliwiec.com>
|
||||
Copyright (c) 2017-2021 Kamil Mysliwiec <https://kamilmysliwiec.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
16
Readme.md
16
Readme.md
@@ -56,16 +56,21 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
|
||||
#### Principal Sponsors
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="180" valign="middle" /></a></td><td>
|
||||
<a href="https://valor-software.com/" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/valor-software.png" width="180" valign="middle" /></a></td>
|
||||
<a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="180" valign="middle" /></a></td>
|
||||
<td>
|
||||
<a href="https://trilon.io" target="_blank"><img src="https://nestjs.com/img/trilon.svg" width="200" valign="middle" /></a></td>
|
||||
</tr></table>
|
||||
|
||||
#### Gold Sponsors
|
||||
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://valor-software.com/" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/valor-software.png" width="170" valign="middle" /></a></td></tr></table>
|
||||
|
||||
#### Silver Sponsors
|
||||
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://neoteric.eu/" target="_blank"><img src="https://nestjs.com/img/neoteric-cut.png" width="120" valign="middle" /></a> </td><td>
|
||||
<a href="http://gojob.com" target="_blank"><img src="http://nestjs.com/img/gojob-logo.png" valign="middle" width="100" /></a> </td><td>
|
||||
<a href="https://trilon.io" target="_blank"><img src="https://nestjs.com/img/trilon.svg" width="170" valign="middle" /></a> </td><td>
|
||||
<a href="http://www.leogistics.com" target="_blank"><img src="https://nestjs.com/img/leogistics-logo.jpeg" width="150" valign="middle" /></td><td>
|
||||
<a href="http://www.meetdandy.com" target="_blank"><img src="https://nestjs.com/img/dandy-wide-logo.png" width="150" valign="middle" /></td></tr></table>
|
||||
|
||||
@@ -90,6 +95,11 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://genuinebee.com/" target="_blank"><img src="https://nestjs.com/img/genuinebee.svg" width="97" valign="middle" /></a> </td>
|
||||
<td align="center" valign="middle"><a href="https://sanyodigital.com/" target="_blank"><img src="https://nestjs.com/img/sanyo-digital.png" width="130" valign="middle" /></a></td></tr><tr><td align="center" valign="middle"><a href="https://vpn-review.com/vpn-for-torrenting" target="_blank"><img src="https://nestjs.com/img/vpn-review-logo.png" width="85" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://lambda-it.ch/" target="_blank"><img src="https://nestjs.com/img/lambda-it-logo.svg" width="115" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://pickwriters.com/top-10-translation-services" target="_blank"><img src="https://nestjs.com/img/pickwriters-logo.png" width="40" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://thewordpoint.com/services/localization" target="_blank"><img src="https://nestjs.com/img/thewordpoint-logo.png" width="40" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://streamat.se/" target="_blank"><img src="https://nestjs.com/img/streamat-logo.png" width="120" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://filmen.nu/" target="_blank"><img src="https://nestjs.com/img/filmen-logo.png" width="120" valign="middle" /></a></td></tr><tr>
|
||||
<td align="center" valign="middle"><a href="https://meercode.io/" target="_blank"><img src="https://nestjs.com/img/meercode-logo.png" width="60" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.najlepszeplatformyforex.pl/blog/broker-xtb/" target="_blank"><img src="https://nestjs.com/img/npf-logo.jpg" width="200" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://thestandarddaily.com/" target="_blank"><img src="https://nestjs.com/img/the-standard-daily-logo.png" width="180" valign="middle" /></a></td>
|
||||
</tr></table>
|
||||
|
||||
## Backers
|
||||
|
||||
177
integration/cors/e2e/express.spec.ts
Normal file
177
integration/cors/e2e/express.spec.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('Express Cors', () => {
|
||||
let app: NestFastifyApplication;
|
||||
const configs = [
|
||||
{
|
||||
origin: 'example.com',
|
||||
methods: 'GET',
|
||||
credentials: true,
|
||||
exposedHeaders: ['foo', 'bar'],
|
||||
allowedHeaders: ['baz', 'woo'],
|
||||
maxAge: 123,
|
||||
},
|
||||
{
|
||||
origin: 'sample.com',
|
||||
methods: 'GET',
|
||||
credentials: true,
|
||||
exposedHeaders: ['zoo', 'bar'],
|
||||
allowedHeaders: ['baz', 'foo'],
|
||||
maxAge: 321,
|
||||
},
|
||||
];
|
||||
describe('Dynamic config', () => {
|
||||
describe('enableCors', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>();
|
||||
|
||||
let requestId = 0;
|
||||
const configDelegation = function (req, cb) {
|
||||
const config = configs[requestId];
|
||||
requestId++;
|
||||
cb(null, config);
|
||||
};
|
||||
app.enableCors(configDelegation);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the first config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the second config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.options('/')
|
||||
.expect('access-control-allow-origin', 'sample.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'zoo,bar')
|
||||
.expect('access-control-allow-methods', 'GET')
|
||||
.expect('access-control-allow-headers', 'baz,foo')
|
||||
.expect('access-control-max-age', '321')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Application Options', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
let requestId = 0;
|
||||
const configDelegation = function (req, cb) {
|
||||
const config = configs[requestId];
|
||||
requestId++;
|
||||
cb(null, config);
|
||||
};
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>(null, {
|
||||
cors: configDelegation,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the first config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the second config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.options('/')
|
||||
.expect('access-control-allow-origin', 'sample.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'zoo,bar')
|
||||
.expect('access-control-allow-methods', 'GET')
|
||||
.expect('access-control-allow-headers', 'baz,foo')
|
||||
.expect('access-control-max-age', '321')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Static config', () => {
|
||||
describe('enableCors', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>();
|
||||
app.enableCors(configs[0]);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`CORS headers`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('Application Options', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>(null, {
|
||||
cors: configs[0],
|
||||
});
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`CORS headers`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
177
integration/cors/e2e/fastify.spec.ts
Normal file
177
integration/cors/e2e/fastify.spec.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('Fastify Cors', () => {
|
||||
let app: NestFastifyApplication;
|
||||
const configs = [
|
||||
{
|
||||
origin: 'example.com',
|
||||
methods: 'GET',
|
||||
credentials: true,
|
||||
exposedHeaders: ['foo', 'bar'],
|
||||
allowedHeaders: ['baz', 'woo'],
|
||||
maxAge: 123,
|
||||
},
|
||||
{
|
||||
origin: 'sample.com',
|
||||
methods: 'GET',
|
||||
credentials: true,
|
||||
exposedHeaders: ['zoo', 'bar'],
|
||||
allowedHeaders: ['baz', 'foo'],
|
||||
maxAge: 321,
|
||||
},
|
||||
];
|
||||
describe('Dynamic config', () => {
|
||||
describe('enableCors', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>();
|
||||
|
||||
let requestId = 0;
|
||||
const configDelegation = function (req, cb) {
|
||||
const config = configs[requestId];
|
||||
requestId++;
|
||||
cb(null, config);
|
||||
};
|
||||
app.enableCors(configDelegation);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the first config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the second config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.options('/')
|
||||
.expect('access-control-allow-origin', 'sample.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'zoo,bar')
|
||||
.expect('access-control-allow-methods', 'GET')
|
||||
.expect('access-control-allow-headers', 'baz,foo')
|
||||
.expect('access-control-max-age', '321')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Application Options', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
let requestId = 0;
|
||||
const configDelegation = function (req, cb) {
|
||||
const config = configs[requestId];
|
||||
requestId++;
|
||||
cb(null, config);
|
||||
};
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>(null, {
|
||||
cors: configDelegation,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the first config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the second config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.options('/')
|
||||
.expect('access-control-allow-origin', 'sample.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'zoo,bar')
|
||||
.expect('access-control-allow-methods', 'GET')
|
||||
.expect('access-control-allow-headers', 'baz,foo')
|
||||
.expect('access-control-max-age', '321')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Static config', () => {
|
||||
describe('enableCors', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>();
|
||||
app.enableCors(configs[0]);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`CORS headers`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
describe('Application Options', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>(null, {
|
||||
cors: configs[0],
|
||||
});
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`CORS headers`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
9
integration/cors/src/app.controller.ts
Normal file
9
integration/cors/src/app.controller.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@Get()
|
||||
getGlobals() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
7
integration/cors/src/app.module.ts
Normal file
7
integration/cors/src/app.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule {}
|
||||
22
integration/cors/tsconfig.json
Normal file
22
integration/cors/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": false,
|
||||
"noImplicitAny": false,
|
||||
"removeComments": true,
|
||||
"noLib": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"e2e/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
]
|
||||
}
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
- "9001:9001"
|
||||
restart: always
|
||||
mysql:
|
||||
image: mysql:5.7.32
|
||||
image: mysql:5.7.33
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: test
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
zookeeper:
|
||||
container_name: test-zookeeper
|
||||
hostname: zookeeper
|
||||
image: confluentinc/cp-zookeeper:5.5.2
|
||||
image: confluentinc/cp-zookeeper:5.5.3
|
||||
ports:
|
||||
- "2181:2181"
|
||||
environment:
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
kafka:
|
||||
container_name: test-kafka
|
||||
hostname: kafka
|
||||
image: confluentinc/cp-kafka:5.5.2
|
||||
image: confluentinc/cp-kafka:5.5.3
|
||||
depends_on:
|
||||
- zookeeper
|
||||
ports:
|
||||
@@ -70,3 +70,4 @@ services:
|
||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
|
||||
KAFKA_DELETE_TOPIC_ENABLE: 'true'
|
||||
|
||||
@@ -63,6 +63,22 @@ describe('Hello world (fastify adapter)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET { host: [":tenant.example1.com", ":tenant.example2.com"] } not matched`, () => {
|
||||
return app
|
||||
.inject({
|
||||
method: 'GET',
|
||||
url: '/host-array',
|
||||
})
|
||||
.then(({ payload }) => {
|
||||
expect(JSON.parse(payload)).to.be.eql({
|
||||
error: 'Internal Server Error',
|
||||
message:
|
||||
'HTTP adapter does not support filtering on hosts: [":tenant.example1.com", ":tenant.example2.com"]',
|
||||
statusCode: 500,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET inject with LightMyRequest chaining API`, () => {
|
||||
return app
|
||||
.inject()
|
||||
|
||||
@@ -28,6 +28,16 @@ describe('Hello world (default adapter)', () => {
|
||||
path: '/host',
|
||||
greeting: 'Host Greeting! tenant=acme',
|
||||
},
|
||||
{
|
||||
host: 'acme.example1.com',
|
||||
path: '/host-array',
|
||||
greeting: 'Host Greeting! tenant=acme',
|
||||
},
|
||||
{
|
||||
host: 'acme.example2.com',
|
||||
path: '/host-array',
|
||||
greeting: 'Host Greeting! tenant=acme',
|
||||
},
|
||||
].forEach(({ host, path, greeting }) => {
|
||||
describe(`host=${host}`, () => {
|
||||
describe('/GET', () => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HelloModule } from './hello/hello.module';
|
||||
import { HostArrayModule } from './host-array/host-array.module';
|
||||
import { HostModule } from './host/host.module';
|
||||
|
||||
@Module({
|
||||
imports: [HelloModule, HostModule],
|
||||
imports: [HelloModule, HostModule, HostArrayModule],
|
||||
})
|
||||
export class ApplicationModule {}
|
||||
|
||||
10
integration/hello-world/src/host-array/dto/test.dto.ts
Normal file
10
integration/hello-world/src/host-array/dto/test.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { IsString, IsNotEmpty, IsNumber } from 'class-validator';
|
||||
|
||||
export class TestDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
string: string;
|
||||
|
||||
@IsNumber()
|
||||
number: number;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Controller, Get, Header, HostParam, Param } from '@nestjs/common';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { HostArrayService } from './host-array.service';
|
||||
import { UserByIdPipe } from './users/user-by-id.pipe';
|
||||
|
||||
@Controller({
|
||||
path: 'host-array',
|
||||
host: [':tenant.example1.com', ':tenant.example2.com'],
|
||||
})
|
||||
export class HostArrayController {
|
||||
constructor(private readonly hostService: HostArrayService) {}
|
||||
|
||||
@Get()
|
||||
@Header('Authorization', 'Bearer')
|
||||
greeting(@HostParam('tenant') tenant: string): string {
|
||||
return `${this.hostService.greeting()} tenant=${tenant}`;
|
||||
}
|
||||
|
||||
@Get('async')
|
||||
async asyncGreeting(@HostParam('tenant') tenant: string): Promise<string> {
|
||||
return `${await this.hostService.greeting()} tenant=${tenant}`;
|
||||
}
|
||||
|
||||
@Get('stream')
|
||||
streamGreeting(@HostParam('tenant') tenant: string): Observable<string> {
|
||||
return of(`${this.hostService.greeting()} tenant=${tenant}`);
|
||||
}
|
||||
|
||||
@Get('local-pipe/:id')
|
||||
localPipe(
|
||||
@Param('id', UserByIdPipe)
|
||||
user: any,
|
||||
@HostParam('tenant') tenant: string,
|
||||
): any {
|
||||
return { ...user, tenant };
|
||||
}
|
||||
}
|
||||
10
integration/hello-world/src/host-array/host-array.module.ts
Normal file
10
integration/hello-world/src/host-array/host-array.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HostArrayController } from './host-array.controller';
|
||||
import { HostArrayService } from './host-array.service';
|
||||
import { UsersService } from './users/users.service';
|
||||
|
||||
@Module({
|
||||
controllers: [HostArrayController],
|
||||
providers: [HostArrayService, UsersService],
|
||||
})
|
||||
export class HostArrayModule {}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class HostArrayService {
|
||||
greeting(): string {
|
||||
return 'Host Greeting!';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserByIdPipe implements PipeTransform<string> {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
transform(value: string, metadata: ArgumentMetadata) {
|
||||
return this.usersService.findById(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
findById(id: string) {
|
||||
return { id, host: true };
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class TestInjectable
|
||||
class AppModule {}
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, { logger: true });
|
||||
const app = await NestFactory.create(AppModule, { logger: false });
|
||||
|
||||
if (SIGNAL_TO_LISTEN && SIGNAL_TO_LISTEN !== 'NONE') {
|
||||
app.enableShutdownHooks([SIGNAL_TO_LISTEN]);
|
||||
|
||||
291
integration/microservices/e2e/concurrent-kafka.spec.ts
Normal file
291
integration/microservices/e2e/concurrent-kafka.spec.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { INestApplication, Logger } from '@nestjs/common';
|
||||
import { Transport } from '@nestjs/microservices';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { Admin, ITopicMetadata, Kafka } from 'kafkajs';
|
||||
import * as request from 'supertest';
|
||||
import * as util from 'util';
|
||||
import { KafkaConcurrentController } from '../src/kafka-concurrent/kafka-concurrent.controller';
|
||||
import { KafkaConcurrentMessagesController } from '../src/kafka-concurrent/kafka-concurrent.messages.controller';
|
||||
|
||||
describe('Kafka concurrent', function () {
|
||||
const numbersOfServers = 3;
|
||||
|
||||
const requestTopic = 'math.sum.sync.number.wait';
|
||||
const responseTopic = 'math.sum.sync.number.wait.reply';
|
||||
|
||||
let admin: Admin;
|
||||
const servers: any[] = [];
|
||||
const apps: INestApplication[] = [];
|
||||
|
||||
const logger = new Logger('concurrent-kafka.spec.ts');
|
||||
|
||||
// set timeout to be longer (especially for the after hook)
|
||||
this.timeout(30000);
|
||||
|
||||
const startServer = async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [
|
||||
KafkaConcurrentController,
|
||||
KafkaConcurrentMessagesController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
// use our own logger for a little
|
||||
// Logger.overrideLogger(new Logger());
|
||||
|
||||
const app = module.createNestApplication();
|
||||
|
||||
const server = app.getHttpAdapter().getInstance();
|
||||
|
||||
app.connectMicroservice({
|
||||
transport: Transport.KAFKA,
|
||||
options: {
|
||||
client: {
|
||||
brokers: ['localhost:9092'],
|
||||
},
|
||||
run: {
|
||||
partitionsConsumedConcurrently: numbersOfServers,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// enable these for clean shutdown
|
||||
app.enableShutdownHooks();
|
||||
|
||||
// push to the collection
|
||||
servers.push(server);
|
||||
apps.push(app);
|
||||
|
||||
// await the start
|
||||
await app.startAllMicroservicesAsync();
|
||||
await app.init();
|
||||
};
|
||||
|
||||
it(`Create kafka topics/partitions`, async () => {
|
||||
const kafka = new Kafka({
|
||||
clientId: 'concurrent-test-admin',
|
||||
brokers: ['localhost:9092'],
|
||||
});
|
||||
|
||||
admin = kafka.admin();
|
||||
await admin.connect();
|
||||
|
||||
let topicMetadata: {
|
||||
topics: ITopicMetadata[];
|
||||
};
|
||||
|
||||
try {
|
||||
topicMetadata = await admin.fetchTopicMetadata({
|
||||
topics: [requestTopic, responseTopic],
|
||||
});
|
||||
} catch (e) {
|
||||
// create with number of servers
|
||||
try {
|
||||
await admin.createTopics({
|
||||
topics: [
|
||||
{
|
||||
topic: requestTopic,
|
||||
numPartitions: numbersOfServers,
|
||||
replicationFactor: 1,
|
||||
},
|
||||
{
|
||||
topic: responseTopic,
|
||||
numPartitions: numbersOfServers,
|
||||
replicationFactor: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(util.format('Create topics error: %o', e));
|
||||
}
|
||||
}
|
||||
|
||||
if (topicMetadata && topicMetadata.topics.length > 0) {
|
||||
// we have topics, how many partitions do they have?
|
||||
for (const topic of topicMetadata.topics) {
|
||||
if (topic.partitions.length < numbersOfServers) {
|
||||
try {
|
||||
await admin.createPartitions({
|
||||
topicPartitions: [
|
||||
{
|
||||
topic: topic.name,
|
||||
count: numbersOfServers,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(util.format('Create partitions error: %o', e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create with number of servers
|
||||
try {
|
||||
await admin.createTopics({
|
||||
topics: [
|
||||
{
|
||||
topic: requestTopic,
|
||||
numPartitions: numbersOfServers,
|
||||
replicationFactor: 1,
|
||||
},
|
||||
{
|
||||
topic: responseTopic,
|
||||
numPartitions: numbersOfServers,
|
||||
replicationFactor: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(util.format('Create topics error: %o', e));
|
||||
}
|
||||
|
||||
// disconnect
|
||||
await admin.disconnect();
|
||||
});
|
||||
|
||||
it(`Start Kafka apps`, async () => {
|
||||
// start all at once
|
||||
await Promise.all(
|
||||
Array(numbersOfServers)
|
||||
.fill(1)
|
||||
.map(async (v, i) => {
|
||||
// return startServer();
|
||||
|
||||
// wait in intervals so the consumers start in order
|
||||
return new Promise<void>(resolve => {
|
||||
setTimeout(async () => {
|
||||
await startServer();
|
||||
|
||||
return resolve();
|
||||
}, 1000 * i);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}).timeout(30000);
|
||||
|
||||
it(`Concurrent messages without forcing a rebalance.`, async () => {
|
||||
// wait a second before notifying the servers to respond
|
||||
setTimeout(async () => {
|
||||
// notify the other servers that it is time to respond
|
||||
await Promise.all(
|
||||
servers.map(async server => {
|
||||
// send to all servers since indexes don't necessarily align with server consumers
|
||||
return request(server).post('/go').send();
|
||||
}),
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
await Promise.all(
|
||||
servers.map(async (server, index) => {
|
||||
// send requests
|
||||
const payload = {
|
||||
key: index,
|
||||
numbers: [1, index],
|
||||
};
|
||||
const result = (1 + index).toString();
|
||||
|
||||
return request(server)
|
||||
.post('/mathSumSyncNumberWait')
|
||||
.send(payload)
|
||||
.expect(200)
|
||||
.expect(200, result);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it(`Close kafka client consumer while waiting for message pattern response.`, async () => {
|
||||
await Promise.all(
|
||||
servers.map(async (server, index) => {
|
||||
// shut off and delete the leader
|
||||
if (index === 0) {
|
||||
return new Promise<void>(resolve => {
|
||||
// wait a second before closing so the producers can send the message to the server consumers
|
||||
setTimeout(async () => {
|
||||
// get the controller
|
||||
const controller = apps[index].get(KafkaConcurrentController);
|
||||
|
||||
// close the controller clients
|
||||
await controller.client.close();
|
||||
|
||||
// notify the other servers that we have stopped
|
||||
await Promise.all(
|
||||
servers.map(async server => {
|
||||
// send to all servers since indexes don't necessarily align with server consumers
|
||||
return request(server).post('/go').send();
|
||||
}),
|
||||
);
|
||||
|
||||
return resolve();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// send requests
|
||||
const payload = {
|
||||
key: index,
|
||||
numbers: [1, index],
|
||||
};
|
||||
const result = (1 + index).toString();
|
||||
|
||||
return request(server)
|
||||
.post('/mathSumSyncNumberWait')
|
||||
.send(payload)
|
||||
.expect(200)
|
||||
.expect(200, result);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it(`Start kafka client consumer while waiting for message pattern response.`, async () => {
|
||||
await Promise.all(
|
||||
servers.map(async (server, index) => {
|
||||
// shut off and delete the leader
|
||||
if (index === 0) {
|
||||
return new Promise<void>(resolve => {
|
||||
// wait a second before closing so the producers can send the message to the server consumers
|
||||
setTimeout(async () => {
|
||||
// get the controller
|
||||
const controller = apps[index].get(KafkaConcurrentController);
|
||||
|
||||
// connect the controller client
|
||||
await controller.client.connect();
|
||||
|
||||
// notify the servers that we have started
|
||||
await Promise.all(
|
||||
servers.map(async server => {
|
||||
// send to all servers since indexes don't necessarily align with server consumers
|
||||
return request(server).post('/go').send();
|
||||
}),
|
||||
);
|
||||
|
||||
return resolve();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// send requests
|
||||
const payload = {
|
||||
key: index,
|
||||
numbers: [1, index],
|
||||
};
|
||||
const result = (1 + index).toString();
|
||||
|
||||
return request(server)
|
||||
.post('/mathSumSyncNumberWait')
|
||||
.send(payload)
|
||||
.expect(200)
|
||||
.expect(200, result);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
after(`Stopping Kafka app`, async () => {
|
||||
// close all concurrently
|
||||
return Promise.all(
|
||||
apps.map(async app => {
|
||||
return app.close();
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,10 +9,13 @@ import { UserEntity } from '../src/kafka/entities/user.entity';
|
||||
import { KafkaController } from '../src/kafka/kafka.controller';
|
||||
import { KafkaMessagesController } from '../src/kafka/kafka.messages.controller';
|
||||
|
||||
describe('Kafka transport', () => {
|
||||
describe('Kafka transport', function () {
|
||||
let server;
|
||||
let app: INestApplication;
|
||||
|
||||
// set timeout to be longer (especially for the after hook)
|
||||
this.timeout(30000);
|
||||
|
||||
it(`Start Kafka app`, async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [KafkaController, KafkaMessagesController],
|
||||
@@ -29,6 +32,7 @@ describe('Kafka transport', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
app.enableShutdownHooks();
|
||||
await app.startAllMicroservicesAsync();
|
||||
await app.init();
|
||||
}).timeout(30000);
|
||||
|
||||
@@ -105,6 +105,22 @@ describe('RPC transport', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('/POST (custom client)', () => {
|
||||
return request(server)
|
||||
.post('/error?client=custom')
|
||||
.send({})
|
||||
.expect(200)
|
||||
.expect('true');
|
||||
});
|
||||
|
||||
it('/POST (standard client)', () => {
|
||||
return request(server)
|
||||
.post('/error?client=standard')
|
||||
.send({})
|
||||
.expect(200)
|
||||
.expect('false');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -12,15 +12,17 @@ import {
|
||||
EventPattern,
|
||||
MessagePattern,
|
||||
Transport,
|
||||
RpcException,
|
||||
} from '@nestjs/microservices';
|
||||
import { from, Observable, of } from 'rxjs';
|
||||
import { scan } from 'rxjs/operators';
|
||||
import { from, Observable, of, throwError } from 'rxjs';
|
||||
import { catchError, scan } from 'rxjs/operators';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(
|
||||
@Inject('USE_CLASS_CLIENT') private useClassClient: ClientProxy,
|
||||
@Inject('USE_FACTORY_CLIENT') private useFactoryClient: ClientProxy,
|
||||
@Inject('CUSTOM_PROXY_CLIENT') private customClient: ClientProxy,
|
||||
) {}
|
||||
static IS_NOTIFIED = false;
|
||||
|
||||
@@ -75,6 +77,17 @@ export class AppController {
|
||||
.reduce(async (a, b) => (await a) && b);
|
||||
}
|
||||
|
||||
@Post('error')
|
||||
@HttpCode(200)
|
||||
serializeError(@Query('client') query: 'custom' | 'standard' = 'standard', @Body() body: Record<string, any>): Observable<boolean> {
|
||||
const client = query === 'custom' ? this.customClient : this.client;
|
||||
return client.send({ cmd: 'err' }, {}).pipe(
|
||||
catchError((err) => {
|
||||
return of(err instanceof RpcException);
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@MessagePattern({ cmd: 'sum' })
|
||||
sum(data: number[]): number {
|
||||
return (data || []).reduce((a, b) => a + b);
|
||||
@@ -95,6 +108,11 @@ export class AppController {
|
||||
return from(data);
|
||||
}
|
||||
|
||||
@MessagePattern({ cmd: 'err' })
|
||||
throwAnError() {
|
||||
return throwError(new Error('err'));
|
||||
}
|
||||
|
||||
@Post('notify')
|
||||
async sendNotification(): Promise<any> {
|
||||
return this.client.emit<number>('notification', true);
|
||||
|
||||
@@ -5,8 +5,16 @@ import {
|
||||
Transport,
|
||||
ClientsModuleOptionsFactory,
|
||||
ClientOptions,
|
||||
ClientTCP,
|
||||
RpcException,
|
||||
} from '@nestjs/microservices';
|
||||
|
||||
class ErrorHandlingProxy extends ClientTCP {
|
||||
serializeError(err) {
|
||||
return new RpcException(err);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class ConfigService {
|
||||
private readonly config = {
|
||||
@@ -51,7 +59,14 @@ class ClientOptionService implements ClientsModuleOptionsFactory {
|
||||
name: 'USE_CLASS_CLIENT',
|
||||
useClass: ClientOptionService,
|
||||
inject: [ConfigService],
|
||||
},
|
||||
}, {
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
name: 'CUSTOM_PROXY_CLIENT',
|
||||
useFactory: (config: ConfigService) => ({
|
||||
customClass: ErrorHandlingProxy
|
||||
})
|
||||
}
|
||||
]),
|
||||
],
|
||||
controllers: [AppController],
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export class SumDto {
|
||||
key: string;
|
||||
numbers: number[];
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
OnModuleDestroy,
|
||||
OnModuleInit,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { Client, ClientKafka, Transport } from '@nestjs/microservices';
|
||||
import { PartitionerArgs } from 'kafkajs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SumDto } from './dto/sum.dto';
|
||||
|
||||
/**
|
||||
* The following function explicity sends messages to the key representing the partition.
|
||||
*/
|
||||
const explicitPartitioner = () => {
|
||||
return ({ message }: PartitionerArgs) => {
|
||||
return parseFloat(message.headers.toPartition.toString());
|
||||
};
|
||||
};
|
||||
|
||||
@Controller()
|
||||
export class KafkaConcurrentController
|
||||
implements OnModuleInit, OnModuleDestroy {
|
||||
protected readonly logger = new Logger(KafkaConcurrentController.name);
|
||||
|
||||
@Client({
|
||||
transport: Transport.KAFKA,
|
||||
options: {
|
||||
client: {
|
||||
brokers: ['localhost:9092'],
|
||||
},
|
||||
run: {
|
||||
partitionsConsumedConcurrently: 3,
|
||||
},
|
||||
producer: {
|
||||
createPartitioner: explicitPartitioner,
|
||||
},
|
||||
},
|
||||
})
|
||||
public readonly client: ClientKafka;
|
||||
|
||||
async onModuleInit() {
|
||||
const requestPatterns = ['math.sum.sync.number.wait'];
|
||||
|
||||
requestPatterns.forEach(pattern => {
|
||||
this.client.subscribeToResponseOf(pattern);
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.client.close();
|
||||
}
|
||||
|
||||
@Post('mathSumSyncNumberWait')
|
||||
@HttpCode(200)
|
||||
public mathSumSyncNumberWait(@Body() data: SumDto): Observable<string> {
|
||||
return this.client.send('math.sum.sync.number.wait', {
|
||||
headers: {
|
||||
toPartition: data.key.toString(),
|
||||
},
|
||||
key: data.key.toString(),
|
||||
value: data.numbers,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Controller, HttpCode, Post } from '@nestjs/common';
|
||||
import { MessagePattern } from '@nestjs/microservices';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { first, map, skipWhile } from 'rxjs/operators';
|
||||
|
||||
@Controller()
|
||||
export class KafkaConcurrentMessagesController {
|
||||
public waiting = new BehaviorSubject<boolean>(false);
|
||||
|
||||
@Post('go')
|
||||
@HttpCode(200)
|
||||
async go() {
|
||||
// no longer waiting
|
||||
this.waiting.next(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@MessagePattern('math.sum.sync.number.wait')
|
||||
public mathSumSyncNumberWait(data: any): Observable<number> {
|
||||
// start waiting
|
||||
this.waiting.next(true);
|
||||
|
||||
// find sum
|
||||
const sum = data.value[0] + data.value[1];
|
||||
|
||||
return this.waiting.asObservable().pipe(
|
||||
skipWhile(isWaiting => {
|
||||
return isWaiting;
|
||||
}),
|
||||
map(() => {
|
||||
return sum;
|
||||
}),
|
||||
first(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Body, Controller, HttpCode, OnModuleInit, Post } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
OnModuleInit,
|
||||
Post,
|
||||
OnModuleDestroy,
|
||||
} from '@nestjs/common';
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { Client, ClientKafka, Transport } from '@nestjs/microservices';
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -6,7 +13,7 @@ import { BusinessDto } from './dtos/business.dto';
|
||||
import { UserDto } from './dtos/user.dto';
|
||||
|
||||
@Controller()
|
||||
export class KafkaController implements OnModuleInit {
|
||||
export class KafkaController implements OnModuleInit, OnModuleDestroy {
|
||||
protected readonly logger = new Logger(KafkaController.name);
|
||||
static IS_NOTIFIED = false;
|
||||
static MATH_SUM = 0;
|
||||
@@ -21,7 +28,7 @@ export class KafkaController implements OnModuleInit {
|
||||
})
|
||||
private readonly client: ClientKafka;
|
||||
|
||||
onModuleInit() {
|
||||
async onModuleInit() {
|
||||
const requestPatterns = [
|
||||
'math.sum.sync.kafka.message',
|
||||
'math.sum.sync.without.key',
|
||||
@@ -36,6 +43,12 @@ export class KafkaController implements OnModuleInit {
|
||||
requestPatterns.forEach(pattern => {
|
||||
this.client.subscribeToResponseOf(pattern);
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.client.close();
|
||||
}
|
||||
|
||||
// sync send kafka message
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('ErrorGateway', () => {
|
||||
ws.emit('push', {
|
||||
test: 'test',
|
||||
});
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('exception', data => {
|
||||
expect(data).to.be.eql({
|
||||
status: 'error',
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('WebSocketGateway (ack)', () => {
|
||||
await app.listenAsync(3000);
|
||||
|
||||
ws = io.connect('http://localhost:8080');
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.emit('push', { test: 'test' }, data => {
|
||||
expect(data).to.be.eql('pong');
|
||||
resolve();
|
||||
@@ -33,7 +33,7 @@ describe('WebSocketGateway (ack)', () => {
|
||||
await app.listenAsync(3000);
|
||||
|
||||
ws = io.connect('http://localhost:8080');
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.emit('push', data => {
|
||||
expect(data).to.be.eql('pong');
|
||||
resolve();
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('WebSocketGateway', () => {
|
||||
ws.emit('push', {
|
||||
test: 'test',
|
||||
});
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('pop', data => {
|
||||
expect(data.test).to.be.eql('test');
|
||||
resolve();
|
||||
@@ -41,7 +41,7 @@ describe('WebSocketGateway', () => {
|
||||
ws.emit('push', {
|
||||
test: 'test',
|
||||
});
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('pop', data => {
|
||||
expect(data.test).to.be.eql('test');
|
||||
resolve();
|
||||
@@ -58,7 +58,7 @@ describe('WebSocketGateway', () => {
|
||||
ws.emit('push', {
|
||||
test: 'test',
|
||||
});
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('pop', data => {
|
||||
expect(data.test).to.be.eql('test');
|
||||
resolve();
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('WebSocketGateway (WsAdapter)', () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('message', data => {
|
||||
expect(JSON.parse(data).data.test).to.be.eql('test');
|
||||
resolve();
|
||||
@@ -57,7 +57,7 @@ describe('WebSocketGateway (WsAdapter)', () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('message', data => {
|
||||
expect(JSON.parse(data).data.test).to.be.eql('test');
|
||||
resolve();
|
||||
@@ -77,7 +77,7 @@ describe('WebSocketGateway (WsAdapter)', () => {
|
||||
ws = new WebSocket('ws://localhost:8080');
|
||||
ws2 = new WebSocket('ws://localhost:8090');
|
||||
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('open', () => {
|
||||
ws.on('message', data => {
|
||||
expect(JSON.parse(data).data.test).to.be.eql('test');
|
||||
@@ -94,7 +94,7 @@ describe('WebSocketGateway (WsAdapter)', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await new Promise(resolve => {
|
||||
await new Promise<void>(resolve => {
|
||||
ws2.on('message', data => {
|
||||
expect(JSON.parse(data).data.test).to.be.eql('test');
|
||||
resolve();
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "7.5.5"
|
||||
"version": "7.6.10"
|
||||
}
|
||||
|
||||
2586
package-lock.json
generated
2586
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
101
package.json
101
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/core",
|
||||
"version": "7.5.3",
|
||||
"version": "7.6.8",
|
||||
"description": "Modern, fast, powerful node.js web framework",
|
||||
"homepage": "https://nestjs.com",
|
||||
"repository": {
|
||||
@@ -29,9 +29,10 @@
|
||||
"test:docker:up": "docker-compose -f integration/docker-compose.yml up -d",
|
||||
"test:docker:down": "docker-compose -f integration/docker-compose.yml down",
|
||||
"lint": "concurrently 'npm run lint:packages' 'npm run lint:integration' 'npm run lint:spec'",
|
||||
"lint:integration": "eslint 'integration/*/{,!(node_modules)/**/}/*.ts' -c '.eslintrc.spec.js' --fix",
|
||||
"lint:packages": "eslint 'packages/**/**.ts' --fix --ignore-pattern 'packages/**/*.spec.ts'",
|
||||
"lint:spec": "eslint 'packages/**/**.spec.ts' -c '.eslintrc.spec.js' --fix",
|
||||
"lint:fix": "concurrently 'npm run lint:packages -- --fix' 'npm run lint:integration -- --fix' 'npm run lint:spec -- --fix'",
|
||||
"lint:integration": "eslint 'integration/*/{,!(node_modules)/**/}/*.ts' -c '.eslintrc.spec.js'",
|
||||
"lint:packages": "eslint 'packages/**/**.ts' --ignore-pattern 'packages/**/*.spec.ts'",
|
||||
"lint:spec": "eslint 'packages/**/**.spec.ts' -c '.eslintrc.spec.js'",
|
||||
"prerelease": "gulp copy-misc && gulp build --dist node_modules/@nestjs",
|
||||
"publish": "npm run prerelease && npm run build:prod && ./node_modules/.bin/lerna publish --force-publish --access public --exact -m \"chore(@nestjs) publish %s release\"",
|
||||
"publish:beta": "npm run prerelease && npm run build:prod && ./node_modules/.bin/lerna publish --npm-tag=beta --access public -m \"chore(@nestjs) publish %s release\"",
|
||||
@@ -52,52 +53,52 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/opencollective": "0.3.2",
|
||||
"axios": "0.21.0",
|
||||
"class-transformer": "0.3.1",
|
||||
"class-validator": "0.12.2",
|
||||
"axios": "0.21.1",
|
||||
"class-transformer": "0.3.2",
|
||||
"class-validator": "0.13.1",
|
||||
"cli-color": "2.0.0",
|
||||
"cors": "2.8.5",
|
||||
"express": "4.17.1",
|
||||
"fast-json-stringify": "2.2.10",
|
||||
"fast-json-stringify": "2.4.1",
|
||||
"fast-safe-stringify": "2.0.7",
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "2.0.3",
|
||||
"object-hash": "2.1.1",
|
||||
"path-to-regexp": "3.2.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "6.6.3",
|
||||
"socket.io": "2.3.0",
|
||||
"uuid": "8.3.1",
|
||||
"tslib": "2.0.3"
|
||||
"socket.io": "2.4.1",
|
||||
"tslib": "2.1.0",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codechecks/client": "0.1.10",
|
||||
"@commitlint/cli": "11.0.0",
|
||||
"@commitlint/config-angular": "11.0.0",
|
||||
"@grpc/proto-loader": "0.5.5",
|
||||
"@nestjs/graphql": "7.9.1",
|
||||
"@nestjs/mongoose": "7.1.2",
|
||||
"@grpc/proto-loader": "0.5.6",
|
||||
"@nestjs/graphql": "7.9.8",
|
||||
"@nestjs/mongoose": "7.2.2",
|
||||
"@nestjs/typeorm": "7.1.5",
|
||||
"@types/amqplib": "0.5.16",
|
||||
"@types/amqplib": "0.5.17",
|
||||
"@types/bytes": "3.1.0",
|
||||
"@types/cache-manager": "2.10.3",
|
||||
"@types/cache-manager": "3.4.0",
|
||||
"@types/chai": "4.2.14",
|
||||
"@types/chai-as-promised": "7.1.3",
|
||||
"@types/cors": "2.8.8",
|
||||
"@types/express": "4.17.9",
|
||||
"@types/gulp": "4.0.7",
|
||||
"@types/mocha": "8.0.4",
|
||||
"@types/mongoose": "5.10.2",
|
||||
"@types/node": "14.14.10",
|
||||
"@types/cors": "2.8.9",
|
||||
"@types/express": "4.17.11",
|
||||
"@types/gulp": "4.0.8",
|
||||
"@types/mocha": "8.2.0",
|
||||
"@types/mongoose": "5.10.3",
|
||||
"@types/node": "14.14.22",
|
||||
"@types/redis": "2.8.28",
|
||||
"@types/reflect-metadata": "0.1.0",
|
||||
"@types/sinon": "9.0.9",
|
||||
"@types/socket.io": "2.1.12",
|
||||
"@types/sinon": "9.0.10",
|
||||
"@types/socket.io": "2.1.13",
|
||||
"@types/ws": "7.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.9.1",
|
||||
"@typescript-eslint/parser": "4.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.14.2",
|
||||
"@typescript-eslint/parser": "4.14.2",
|
||||
"amqp-connection-manager": "3.2.1",
|
||||
"amqplib": "0.6.0",
|
||||
"apollo-server-express": "2.19.0",
|
||||
"apollo-server-express": "2.19.2",
|
||||
"artillery": "1.6.1",
|
||||
"awesome-typescript-loader": "5.2.1",
|
||||
"body-parser": "1.19.0",
|
||||
@@ -105,25 +106,25 @@
|
||||
"cache-manager": "3.4.0",
|
||||
"chai": "4.2.0",
|
||||
"chai-as-promised": "7.1.1",
|
||||
"clang-format": "1.4.0",
|
||||
"clang-format": "1.5.0",
|
||||
"commitlint-circle": "1.0.0",
|
||||
"concurrently": "5.3.0",
|
||||
"conventional-changelog": "3.1.24",
|
||||
"core-js": "3.8.1",
|
||||
"core-js": "3.8.3",
|
||||
"coveralls": "3.1.0",
|
||||
"delete-empty": "3.0.0",
|
||||
"engine.io-client": "4.0.4",
|
||||
"eslint": "7.15.0",
|
||||
"eslint-config-prettier": "7.0.0",
|
||||
"engine.io-client": "4.1.1",
|
||||
"eslint": "7.19.0",
|
||||
"eslint-config-prettier": "7.2.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eventsource": "1.0.7",
|
||||
"fancy-log": "1.3.3",
|
||||
"fastify": "3.9.1",
|
||||
"fastify-cors": "5.0.0",
|
||||
"fastify": "3.11.0",
|
||||
"fastify-cors": "5.2.0",
|
||||
"fastify-formbody": "5.0.0",
|
||||
"fastify-multipart": "3.3.1",
|
||||
"fastify-static": "3.3.0",
|
||||
"graphql": "15.4.0",
|
||||
"fastify-static": "3.4.0",
|
||||
"graphql": "15.5.0",
|
||||
"graphql-tools": "7.0.2",
|
||||
"grpc": "1.24.4",
|
||||
"gulp": "4.0.2",
|
||||
@@ -132,39 +133,39 @@
|
||||
"gulp-sourcemaps": "3.0.0",
|
||||
"gulp-typescript": "5.0.1",
|
||||
"gulp-watch": "5.0.1",
|
||||
"husky": "4.3.5",
|
||||
"imports-loader": "1.2.0",
|
||||
"husky": "4.3.8",
|
||||
"imports-loader": "2.0.0",
|
||||
"json-loader": "0.5.7",
|
||||
"kafkajs": "1.12.0",
|
||||
"kafkajs": "1.15.0",
|
||||
"lerna": "2.11.0",
|
||||
"light-my-request": "4.3.0",
|
||||
"light-my-request": "4.4.1",
|
||||
"lint-staged": "10.5.3",
|
||||
"markdown-table": "2.0.0",
|
||||
"merge-graphql-schemas": "1.7.8",
|
||||
"middie": "5.2.0",
|
||||
"mocha": "8.2.1",
|
||||
"mongoose": "5.11.5",
|
||||
"mongoose": "5.11.14",
|
||||
"mqtt": "4.2.6",
|
||||
"multer": "1.4.2",
|
||||
"mysql": "2.18.1",
|
||||
"nats": "1.4.12",
|
||||
"nodemon": "2.0.6",
|
||||
"nodemon": "2.0.7",
|
||||
"nyc": "15.1.0",
|
||||
"point-of-view": "4.7.0",
|
||||
"point-of-view": "4.11.0",
|
||||
"prettier": "2.2.1",
|
||||
"redis": "3.0.2",
|
||||
"rxjs-compat": "6.6.3",
|
||||
"sinon": "9.2.1",
|
||||
"sinon": "9.2.4",
|
||||
"sinon-chai": "3.5.0",
|
||||
"socket.io-client": "2.3.1",
|
||||
"socket.io-client": "2.4.0",
|
||||
"subscriptions-transport-ws": "0.9.18",
|
||||
"supertest": "6.0.1",
|
||||
"supertest": "6.1.3",
|
||||
"ts-morph": "9.1.0",
|
||||
"ts-node": "9.1.1",
|
||||
"typeorm": "0.2.29",
|
||||
"typescript": "4.0.3",
|
||||
"typeorm": "0.2.30",
|
||||
"typescript": "4.1.3",
|
||||
"wrk": "1.2.1",
|
||||
"ws": "7.4.1"
|
||||
"ws": "7.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
|
||||
@@ -56,16 +56,21 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
|
||||
#### Principal Sponsors
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="180" valign="middle" /></a></td><td>
|
||||
<a href="https://valor-software.com/" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/valor-software.png" width="180" valign="middle" /></a></td>
|
||||
<a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="180" valign="middle" /></a></td>
|
||||
<td>
|
||||
<a href="https://trilon.io" target="_blank"><img src="https://nestjs.com/img/trilon.svg" width="200" valign="middle" /></a></td>
|
||||
</tr></table>
|
||||
|
||||
#### Gold Sponsors
|
||||
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://valor-software.com/" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/valor-software.png" width="170" valign="middle" /></a></td></tr></table>
|
||||
|
||||
#### Silver Sponsors
|
||||
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://neoteric.eu/" target="_blank"><img src="https://nestjs.com/img/neoteric-cut.png" width="120" valign="middle" /></a> </td><td>
|
||||
<a href="http://gojob.com" target="_blank"><img src="http://nestjs.com/img/gojob-logo.png" valign="middle" width="100" /></a> </td><td>
|
||||
<a href="https://trilon.io" target="_blank"><img src="https://nestjs.com/img/trilon.svg" width="170" valign="middle" /></a> </td><td>
|
||||
<a href="http://www.leogistics.com" target="_blank"><img src="https://nestjs.com/img/leogistics-logo.jpeg" width="150" valign="middle" /></td><td>
|
||||
<a href="http://www.meetdandy.com" target="_blank"><img src="https://nestjs.com/img/dandy-wide-logo.png" width="150" valign="middle" /></td></tr></table>
|
||||
|
||||
@@ -90,6 +95,11 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://genuinebee.com/" target="_blank"><img src="https://nestjs.com/img/genuinebee.svg" width="97" valign="middle" /></a> </td>
|
||||
<td align="center" valign="middle"><a href="https://sanyodigital.com/" target="_blank"><img src="https://nestjs.com/img/sanyo-digital.png" width="130" valign="middle" /></a></td></tr><tr><td align="center" valign="middle"><a href="https://vpn-review.com/vpn-for-torrenting" target="_blank"><img src="https://nestjs.com/img/vpn-review-logo.png" width="85" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://lambda-it.ch/" target="_blank"><img src="https://nestjs.com/img/lambda-it-logo.svg" width="115" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://pickwriters.com/top-10-translation-services" target="_blank"><img src="https://nestjs.com/img/pickwriters-logo.png" width="40" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://thewordpoint.com/services/localization" target="_blank"><img src="https://nestjs.com/img/thewordpoint-logo.png" width="40" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://streamat.se/" target="_blank"><img src="https://nestjs.com/img/streamat-logo.png" width="120" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://filmen.nu/" target="_blank"><img src="https://nestjs.com/img/filmen-logo.png" width="120" valign="middle" /></a></td></tr><tr>
|
||||
<td align="center" valign="middle"><a href="https://meercode.io/" target="_blank"><img src="https://nestjs.com/img/meercode-logo.png" width="60" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.najlepszeplatformyforex.pl/blog/broker-xtb/" target="_blank"><img src="https://nestjs.com/img/npf-logo.jpg" width="200" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://thestandarddaily.com/" target="_blank"><img src="https://nestjs.com/img/the-standard-daily-logo.png" width="180" valign="middle" /></a></td>
|
||||
</tr></table>
|
||||
|
||||
## Backers
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface ControllerOptions extends ScopeOptions {
|
||||
*
|
||||
* @see [Routing](https://docs.nestjs.com/controllers#routing)
|
||||
*/
|
||||
host?: string;
|
||||
host?: string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -78,4 +78,3 @@ export class HttpException extends Error {
|
||||
: { statusCode, message: objectOrError, error: description };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,4 +50,9 @@ export interface ClassTransformOptions {
|
||||
* and exposing all class properties (with undefined, if nothing else is given)
|
||||
*/
|
||||
excludeExtraneousValues?: boolean;
|
||||
/**
|
||||
* If set to true then class transformer will take default values for unprovided fields.
|
||||
* This is useful when you convert a plain object to a class and have an optional field with a default value.
|
||||
*/
|
||||
exposeDefaultValues?: boolean;
|
||||
}
|
||||
|
||||
@@ -52,3 +52,10 @@ export interface CorsOptions {
|
||||
*/
|
||||
optionsSuccessStatus?: number;
|
||||
}
|
||||
|
||||
export interface CorsOptionsCallback {
|
||||
(error: Error, options: CorsOptions): void;
|
||||
}
|
||||
export interface CorsOptionsDelegate<T> {
|
||||
(req: T, cb: CorsOptionsCallback): void;
|
||||
}
|
||||
|
||||
@@ -2,29 +2,41 @@
|
||||
* Validation error description.
|
||||
* @see https://github.com/typestack/class-validator
|
||||
*
|
||||
* class-validator@0.13.0
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface ValidationError {
|
||||
/**
|
||||
* Object that was validated.
|
||||
*
|
||||
* OPTIONAL - configurable via the ValidatorOptions.validationError.target option
|
||||
*/
|
||||
target: Record<string, any>;
|
||||
target?: Record<string, any>;
|
||||
/**
|
||||
* Object's property that hasn't passed validation.
|
||||
*/
|
||||
property: string;
|
||||
/**
|
||||
* Value that hasn't passed validation.
|
||||
* Value that haven't pass a validation.
|
||||
*
|
||||
* OPTIONAL - configurable via the ValidatorOptions.validationError.value option
|
||||
*/
|
||||
value: any;
|
||||
value?: any;
|
||||
/**
|
||||
* Constraints that failed validation with error messages.
|
||||
*/
|
||||
constraints: {
|
||||
constraints?: {
|
||||
[type: string]: string;
|
||||
};
|
||||
/**
|
||||
* Contains all nested validation errors of the property.
|
||||
*/
|
||||
children: ValidationError[];
|
||||
children?: ValidationError[];
|
||||
/**
|
||||
* A transient set of data passed through to the validation result for response mapping
|
||||
*/
|
||||
contexts?: {
|
||||
[type: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
* Options passed to validator during validation.
|
||||
* @see https://github.com/typestack/class-validator
|
||||
*
|
||||
* class-validator@0.10.1
|
||||
* class-validator@0.13.0
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface ValidatorOptions {
|
||||
/**
|
||||
* If set to true then class-validator will print extra warning messages to the console when something is not right.
|
||||
*/
|
||||
enableDebugMessages?: boolean;
|
||||
/**
|
||||
* If set to true then validator will skip validation of all properties that are undefined in the validating object.
|
||||
*/
|
||||
@@ -33,6 +37,15 @@ export interface ValidatorOptions {
|
||||
* Groups to be used during validation of the object.
|
||||
*/
|
||||
groups?: string[];
|
||||
/**
|
||||
* Set default for `always` option of decorators. Default can be overridden in decorator options.
|
||||
*/
|
||||
always?: boolean;
|
||||
/**
|
||||
* If [groups]{@link ValidatorOptions#groups} is not given or is empty,
|
||||
* ignore decorators with at least one group.
|
||||
*/
|
||||
strictGroups?: boolean;
|
||||
/**
|
||||
* If set to true, the validation will not use default messages.
|
||||
* Error message always will be undefined if its not explicitly set.
|
||||
@@ -55,4 +68,8 @@ export interface ValidatorOptions {
|
||||
* Settings true will cause fail validation of unknown objects.
|
||||
*/
|
||||
forbidUnknownValues?: boolean;
|
||||
/**
|
||||
* When set to true, validation of the given property will stop after encountering the first error. Defaults to false.
|
||||
*/
|
||||
stopAtFirstError?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { RequestMethod } from '../../enums';
|
||||
import { CorsOptions } from '../../interfaces/external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from '../../interfaces/external/cors-options.interface';
|
||||
import { NestApplicationOptions } from '../../interfaces/nest-application-options.interface';
|
||||
|
||||
export type ErrorHandler<TRequest = any, TResponse = any> = (
|
||||
@@ -62,7 +65,7 @@ export interface HttpServer<TRequest = any, TResponse = any> {
|
||||
getRequestUrl?(request: TResponse): string;
|
||||
getInstance(): any;
|
||||
registerParserMiddleware(): any;
|
||||
enableCors(options: CorsOptions): any;
|
||||
enableCors(options: CorsOptions | CorsOptionsDelegate<TRequest>): any;
|
||||
getHttpServer(): any;
|
||||
initHttpServer(options: NestApplicationOptions): void;
|
||||
close(): any;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ShutdownSignal } from '../enums/shutdown-signal.enum';
|
||||
import { LoggerService, LogLevel } from '../services/logger.service';
|
||||
import { Abstract } from './abstract.interface';
|
||||
import { DynamicModule } from './modules';
|
||||
import { Type } from './type.interface';
|
||||
|
||||
/**
|
||||
@@ -13,7 +14,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>): INestApplicationContext;
|
||||
select<T>(module: Type<T> | DynamicModule): INestApplicationContext;
|
||||
|
||||
/**
|
||||
* Retrieves an instance of either injectable or controller, otherwise, throws exception.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { CorsOptions } from './external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from './external/cors-options.interface';
|
||||
import { HttpsOptions } from './external/https-options.interface';
|
||||
import { NestApplicationContextOptions } from './nest-application-context-options.interface';
|
||||
|
||||
@@ -9,7 +12,7 @@ export interface NestApplicationOptions extends NestApplicationContextOptions {
|
||||
/**
|
||||
* CORS options from [CORS package](https://github.com/expressjs/cors#configuration-options)
|
||||
*/
|
||||
cors?: boolean | CorsOptions;
|
||||
cors?: boolean | CorsOptions | CorsOptionsDelegate<any>;
|
||||
/**
|
||||
* Whether to use underlying platform body parser.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { CorsOptions } from './external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from './external/cors-options.interface';
|
||||
import { CanActivate } from './features/can-activate.interface';
|
||||
import { NestInterceptor } from './features/nest-interceptor.interface';
|
||||
import { HttpServer } from './http/http-server.interface';
|
||||
@@ -30,7 +33,7 @@ export interface INestApplication extends INestApplicationContext {
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
enableCors(options?: CorsOptions): void;
|
||||
enableCors(options?: CorsOptions | CorsOptionsDelegate<any>): void;
|
||||
|
||||
/**
|
||||
* Starts the application.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface Type<T> extends Function {
|
||||
export interface Type<T = any> extends Function {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/common",
|
||||
"version": "7.5.5",
|
||||
"version": "7.6.10",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"homepage": "https://nestjs.com",
|
||||
@@ -17,13 +17,27 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "0.21.0",
|
||||
"axios": "0.21.1",
|
||||
"iterare": "1.2.1",
|
||||
"tslib": "2.0.3",
|
||||
"uuid": "8.3.1"
|
||||
"tslib": "2.1.0",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cache-manager": "*",
|
||||
"class-transformer": "*",
|
||||
"class-validator": "*",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"cache-manager": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
},
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,39 @@ export class ParseArrayPipe implements PipeTransform {
|
||||
} catch {}
|
||||
return this.validationPipe.transform(item, validationMetadata);
|
||||
};
|
||||
value = await Promise.all(value.map(toClassInstance));
|
||||
if (this.options.stopAtFirstError === false) {
|
||||
// strict compare to "false" to make sure
|
||||
// that this option is disabled by default
|
||||
let errors = [];
|
||||
|
||||
const targetArray = value as Array<unknown>;
|
||||
for (let i = 0; i < targetArray.length; i++) {
|
||||
try {
|
||||
targetArray[i] = await toClassInstance(targetArray[i]);
|
||||
} catch (err) {
|
||||
let message: string[] | unknown;
|
||||
if (err.getResponse) {
|
||||
const response = err.getResponse();
|
||||
if (Array.isArray(response.message)) {
|
||||
message = response.message.map(
|
||||
(item: string) => `[${i}] ${item}`,
|
||||
);
|
||||
} else {
|
||||
message = `[${i}] ${response.message}`;
|
||||
}
|
||||
} else {
|
||||
message = err;
|
||||
}
|
||||
errors = errors.concat(message);
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw this.exceptionFactory(errors as any);
|
||||
}
|
||||
return targetArray;
|
||||
} else {
|
||||
value = await Promise.all(value.map(toClassInstance));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -199,27 +199,35 @@ export class ValidationPipe implements PipeTransform<any> {
|
||||
|
||||
protected mapChildrenToValidationErrors(
|
||||
error: ValidationError,
|
||||
parentPath?: string,
|
||||
): ValidationError[] {
|
||||
if (!(error.children && error.children.length)) {
|
||||
return [error];
|
||||
}
|
||||
const validationErrors = [];
|
||||
parentPath = parentPath
|
||||
? `${parentPath}.${error.property}`
|
||||
: error.property;
|
||||
for (const item of error.children) {
|
||||
if (item.children && item.children.length) {
|
||||
validationErrors.push(...this.mapChildrenToValidationErrors(item));
|
||||
validationErrors.push(
|
||||
...this.mapChildrenToValidationErrors(item, parentPath),
|
||||
);
|
||||
}
|
||||
validationErrors.push(this.prependConstraintsWithParentProp(error, item));
|
||||
validationErrors.push(
|
||||
this.prependConstraintsWithParentProp(parentPath, item),
|
||||
);
|
||||
}
|
||||
return validationErrors;
|
||||
}
|
||||
|
||||
protected prependConstraintsWithParentProp(
|
||||
parentError: ValidationError,
|
||||
parentPath: string,
|
||||
error: ValidationError,
|
||||
): ValidationError {
|
||||
const constraints = {};
|
||||
for (const key in error.constraints) {
|
||||
constraints[key] = `${parentError.property}.${error.constraints[key]}`;
|
||||
constraints[key] = `${parentPath}.${error.constraints[key]}`;
|
||||
}
|
||||
return {
|
||||
...error,
|
||||
|
||||
@@ -71,7 +71,7 @@ export class ClassSerializerInterceptor implements NestInterceptor {
|
||||
: plainOrClass;
|
||||
}
|
||||
|
||||
private getContextOptions(
|
||||
protected getContextOptions(
|
||||
context: ExecutionContext,
|
||||
): ClassTransformOptions | undefined {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '../decorators/core/injectable.decorator';
|
||||
import { Optional } from '../decorators/core/optional.decorator';
|
||||
import { clc, yellow } from '../utils/cli-colors.util';
|
||||
import { isObject } from '../utils/shared.utils';
|
||||
import { isObject, isPlainObject } from '../utils/shared.utils';
|
||||
|
||||
declare const process: any;
|
||||
|
||||
@@ -83,7 +83,7 @@ export class Logger implements LoggerService {
|
||||
context = '',
|
||||
isTimeDiffEnabled = true,
|
||||
) {
|
||||
this.printMessage(message, clc.red, context, isTimeDiffEnabled);
|
||||
this.printMessage(message, clc.red, context, isTimeDiffEnabled, 'stderr');
|
||||
this.printStackTrace(trace);
|
||||
}
|
||||
|
||||
@@ -144,18 +144,22 @@ export class Logger implements LoggerService {
|
||||
color: (message: string) => string,
|
||||
context = '',
|
||||
isTimeDiffEnabled?: boolean,
|
||||
writeStreamType?: 'stdout' | 'stderr',
|
||||
) {
|
||||
const output = isObject(message)
|
||||
const output = isPlainObject(message)
|
||||
? `${color('Object:')}\n${JSON.stringify(message, null, 2)}\n`
|
||||
: color(message);
|
||||
|
||||
const pidMessage = color(`[Nest] ${process.pid} - `);
|
||||
const contextMessage = context ? yellow(`[${context}] `) : '';
|
||||
const timestampDiff = this.updateAndGetTimestampDiff(isTimeDiffEnabled);
|
||||
const instance = this.instance as typeof Logger;
|
||||
process.stdout.write(
|
||||
`${pidMessage}${instance.getTimestamp()} ${contextMessage}${output}${timestampDiff}\n`,
|
||||
);
|
||||
const instance = (this.instance as typeof Logger) ?? Logger;
|
||||
const timestamp = instance.getTimestamp
|
||||
? instance.getTimestamp()
|
||||
: Logger.getTimestamp?.();
|
||||
const computedMessage = `${pidMessage}${timestamp} ${contextMessage}${output}${timestampDiff}\n`;
|
||||
|
||||
process[writeStreamType ?? 'stdout'].write(computedMessage);
|
||||
}
|
||||
|
||||
private static updateAndGetTimestampDiff(
|
||||
@@ -173,6 +177,6 @@ export class Logger implements LoggerService {
|
||||
if (!trace) {
|
||||
return;
|
||||
}
|
||||
process.stdout.write(`${trace}\n`);
|
||||
process.stderr.write(`${trace}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Controller } from '../../decorators/core/controller.decorator';
|
||||
describe('@Controller', () => {
|
||||
const reflectedPath = 'test';
|
||||
const reflectedHost = 'api.example.com';
|
||||
const reflectedHostArray = ['api1.example.com', 'api2.example.com'];
|
||||
|
||||
@Controller(reflectedPath)
|
||||
class Test {}
|
||||
@@ -14,6 +15,9 @@ describe('@Controller', () => {
|
||||
@Controller({ path: reflectedPath, host: reflectedHost })
|
||||
class PathAndHostDecorator {}
|
||||
|
||||
@Controller({ path: reflectedPath, host: reflectedHostArray })
|
||||
class PathAndHostArrayDecorator {}
|
||||
|
||||
@Controller({ host: reflectedHost })
|
||||
class HostOnlyDecorator {}
|
||||
|
||||
@@ -29,6 +33,8 @@ describe('@Controller', () => {
|
||||
expect(host).to.be.eql(reflectedHost);
|
||||
const host2 = Reflect.getMetadata('host', HostOnlyDecorator);
|
||||
expect(host2).to.be.eql(reflectedHost);
|
||||
const host3 = Reflect.getMetadata('host', PathAndHostArrayDecorator);
|
||||
expect(host3).to.be.eql(reflectedHostArray);
|
||||
});
|
||||
|
||||
it('should set default path when no object passed as param', () => {
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import * as chai from 'chai';
|
||||
import { expect } from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsDefined,
|
||||
IsNumber,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { BadRequestException } from '../../exceptions';
|
||||
import { ArgumentMetadata } from '../../interfaces/features/pipe-transform.interface';
|
||||
import { ParseArrayPipe } from '../../pipes/parse-array.pipe';
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
describe('ParseArrayPipe', () => {
|
||||
let target: ParseArrayPipe;
|
||||
@@ -102,6 +114,153 @@ describe('ParseArrayPipe', () => {
|
||||
expect(item).to.be.instanceOf(ArrItem);
|
||||
});
|
||||
});
|
||||
describe('when "stopAtFirstError" is explicitly turned off', () => {
|
||||
it('should validate each item and concat errors', async () => {
|
||||
class ArrItemWithProp {
|
||||
@IsNumber()
|
||||
number: number;
|
||||
}
|
||||
const pipe = new ParseArrayPipe({
|
||||
items: ArrItemWithProp,
|
||||
stopAtFirstError: false,
|
||||
});
|
||||
try {
|
||||
await pipe.transform(
|
||||
[
|
||||
{ number: '1' },
|
||||
{ number: '1' },
|
||||
{ number: 1 },
|
||||
] as ArrItemWithProp[],
|
||||
{} as ArgumentMetadata,
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err).to.be.instanceOf(BadRequestException);
|
||||
expect(err.getResponse().message).to.deep.equal([
|
||||
'[0] number must be a number conforming to the specified constraints',
|
||||
'[1] number must be a number conforming to the specified constraints',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate each nested object and concat errors', async () => {
|
||||
class RandomObject {
|
||||
@IsDefined()
|
||||
@IsBoolean()
|
||||
isEnabled: boolean;
|
||||
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
constructor(partial: Partial<any>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
class ArrItemObject {
|
||||
@ValidateNested()
|
||||
random: RandomObject;
|
||||
}
|
||||
const pipe = new ParseArrayPipe({
|
||||
items: ArrItemObject,
|
||||
stopAtFirstError: false,
|
||||
});
|
||||
try {
|
||||
await pipe.transform(
|
||||
[
|
||||
{
|
||||
random: new RandomObject({
|
||||
isEnabled: true,
|
||||
title: true,
|
||||
createdAt: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
random: new RandomObject({
|
||||
title: 'ok',
|
||||
createdAt: false,
|
||||
}),
|
||||
},
|
||||
] as any[],
|
||||
{} as ArgumentMetadata,
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err).to.be.instanceOf(BadRequestException);
|
||||
expect(err.getResponse().message).to.deep.equal([
|
||||
'[0] random.title must be a string',
|
||||
'[1] random.isEnabled should not be null or undefined',
|
||||
'[1] random.isEnabled must be a boolean value',
|
||||
'[1] random.createdAt must be a Date instance',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate each nested array and concat errors', async () => {
|
||||
class RandomObject {
|
||||
@IsDefined()
|
||||
@IsBoolean()
|
||||
isEnabled: boolean;
|
||||
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
constructor(partial: Partial<any>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
class ArrItemObject {
|
||||
@Type(() => RandomObject)
|
||||
@ValidateNested({ each: true })
|
||||
random: RandomObject[];
|
||||
}
|
||||
const pipe = new ParseArrayPipe({
|
||||
items: ArrItemObject,
|
||||
stopAtFirstError: false,
|
||||
});
|
||||
try {
|
||||
await pipe.transform(
|
||||
[
|
||||
{
|
||||
random: [
|
||||
new RandomObject({
|
||||
isEnabled: true,
|
||||
title: true,
|
||||
createdAt: new Date(),
|
||||
}),
|
||||
new RandomObject({
|
||||
isEnabled: true,
|
||||
title: true,
|
||||
createdAt: new Date(),
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
random: [
|
||||
new RandomObject({
|
||||
title: 'ok',
|
||||
createdAt: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
] as any[],
|
||||
{} as ArgumentMetadata,
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err).to.be.instanceOf(BadRequestException);
|
||||
expect(err.getResponse().message).to.deep.equal([
|
||||
'[0] random.0.title must be a string',
|
||||
'[0] random.1.title must be a string',
|
||||
'[1] random.0.isEnabled should not be null or undefined',
|
||||
'[1] random.0.isEnabled must be a boolean value',
|
||||
'[1] random.0.createdAt must be a Date instance',
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
IsArray,
|
||||
} from 'class-validator';
|
||||
import { HttpStatus } from '../../enums';
|
||||
import { UnprocessableEntityException } from '../../exceptions';
|
||||
@@ -156,6 +157,32 @@ describe('ValidationPipe', () => {
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
class TestModelForNestedArrayValidation {
|
||||
@IsString()
|
||||
public prop: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested()
|
||||
@Type(() => TestModel2)
|
||||
public test: TestModel2[];
|
||||
}
|
||||
it('should provide complete path for nested errors', async () => {
|
||||
try {
|
||||
const model = new TestModelForNestedArrayValidation();
|
||||
model.test = [new TestModel2()];
|
||||
await target.transform(model, {
|
||||
type: 'body',
|
||||
metatype: TestModelForNestedArrayValidation,
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err.getResponse().message).to.be.eql([
|
||||
'prop must be a string',
|
||||
'test.0.prop1 must be a string',
|
||||
'test.0.prop2 must be a boolean value',
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('when validation transforms', () => {
|
||||
it('should return a TestModel instance', async () => {
|
||||
|
||||
@@ -27,6 +27,12 @@ export const isPlainObject = (fn: any): fn is object => {
|
||||
export const addLeadingSlash = (path?: string): string =>
|
||||
path ? (path.charAt(0) !== '/' ? '/' + path : path) : '';
|
||||
|
||||
/**
|
||||
* Deprecated. Use the "addLeadingSlash" function instead.
|
||||
* @deprecated
|
||||
*/
|
||||
export const validatePath = addLeadingSlash;
|
||||
|
||||
export const isFunction = (fn: any): boolean => typeof fn === 'function';
|
||||
export const isString = (fn: any): fn is string => typeof fn === 'string';
|
||||
export const isConstructor = (fn: any): boolean => fn === 'constructor';
|
||||
|
||||
@@ -56,16 +56,21 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
|
||||
#### Principal Sponsors
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="180" valign="middle" /></a></td><td>
|
||||
<a href="https://valor-software.com/" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/valor-software.png" width="180" valign="middle" /></a></td>
|
||||
<a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="180" valign="middle" /></a></td>
|
||||
<td>
|
||||
<a href="https://trilon.io" target="_blank"><img src="https://nestjs.com/img/trilon.svg" width="200" valign="middle" /></a></td>
|
||||
</tr></table>
|
||||
|
||||
#### Gold Sponsors
|
||||
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://valor-software.com/" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/valor-software.png" width="170" valign="middle" /></a></td></tr></table>
|
||||
|
||||
#### Silver Sponsors
|
||||
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://neoteric.eu/" target="_blank"><img src="https://nestjs.com/img/neoteric-cut.png" width="120" valign="middle" /></a> </td><td>
|
||||
<a href="http://gojob.com" target="_blank"><img src="http://nestjs.com/img/gojob-logo.png" valign="middle" width="100" /></a> </td><td>
|
||||
<a href="https://trilon.io" target="_blank"><img src="https://nestjs.com/img/trilon.svg" width="170" valign="middle" /></a> </td><td>
|
||||
<a href="http://www.leogistics.com" target="_blank"><img src="https://nestjs.com/img/leogistics-logo.jpeg" width="150" valign="middle" /></td><td>
|
||||
<a href="http://www.meetdandy.com" target="_blank"><img src="https://nestjs.com/img/dandy-wide-logo.png" width="150" valign="middle" /></td></tr></table>
|
||||
|
||||
@@ -90,6 +95,11 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://genuinebee.com/" target="_blank"><img src="https://nestjs.com/img/genuinebee.svg" width="97" valign="middle" /></a> </td>
|
||||
<td align="center" valign="middle"><a href="https://sanyodigital.com/" target="_blank"><img src="https://nestjs.com/img/sanyo-digital.png" width="130" valign="middle" /></a></td></tr><tr><td align="center" valign="middle"><a href="https://vpn-review.com/vpn-for-torrenting" target="_blank"><img src="https://nestjs.com/img/vpn-review-logo.png" width="85" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://lambda-it.ch/" target="_blank"><img src="https://nestjs.com/img/lambda-it-logo.svg" width="115" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://pickwriters.com/top-10-translation-services" target="_blank"><img src="https://nestjs.com/img/pickwriters-logo.png" width="40" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://thewordpoint.com/services/localization" target="_blank"><img src="https://nestjs.com/img/thewordpoint-logo.png" width="40" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://streamat.se/" target="_blank"><img src="https://nestjs.com/img/streamat-logo.png" width="120" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://filmen.nu/" target="_blank"><img src="https://nestjs.com/img/filmen-logo.png" width="120" valign="middle" /></a></td></tr><tr>
|
||||
<td align="center" valign="middle"><a href="https://meercode.io/" target="_blank"><img src="https://nestjs.com/img/meercode-logo.png" width="60" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.najlepszeplatformyforex.pl/blog/broker-xtb/" target="_blank"><img src="https://nestjs.com/img/npf-logo.jpg" width="200" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://thestandarddaily.com/" target="_blank"><img src="https://nestjs.com/img/the-standard-daily-logo.png" width="180" valign="middle" /></a></td>
|
||||
</tr></table>
|
||||
|
||||
## Backers
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { HttpServer, RequestMethod } from '@nestjs/common';
|
||||
import { RequestHandler } from '@nestjs/common/interfaces';
|
||||
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';
|
||||
|
||||
/**
|
||||
@@ -14,6 +17,14 @@ export abstract class AbstractHttpAdapter<
|
||||
protected httpServer: TServer;
|
||||
|
||||
constructor(protected readonly instance: any) {}
|
||||
all(path: string, handler: RequestHandler<TRequest, TResponse>);
|
||||
all(handler: RequestHandler<TRequest, TResponse>);
|
||||
all(path: any, handler?: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
setBaseViewsDir?(path: string | string[]): this {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
public async init() {}
|
||||
@@ -97,7 +108,10 @@ export abstract class AbstractHttpAdapter<
|
||||
abstract setNotFoundHandler(handler: Function, prefix?: string);
|
||||
abstract setHeader(response, name: string, value: string);
|
||||
abstract registerParserMiddleware(prefix?: string);
|
||||
abstract enableCors(options: CorsOptions, prefix?: string);
|
||||
abstract enableCors(
|
||||
options: CorsOptions | CorsOptionsDelegate<TRequest>,
|
||||
prefix?: string,
|
||||
);
|
||||
abstract createMiddlewareFactory(
|
||||
requestMethod: RequestMethod,
|
||||
):
|
||||
|
||||
@@ -14,12 +14,8 @@ export class ApplicationConfig {
|
||||
private globalInterceptors: NestInterceptor[] = [];
|
||||
private globalGuards: CanActivate[] = [];
|
||||
private readonly globalRequestPipes: InstanceWrapper<PipeTransform>[] = [];
|
||||
private readonly globalRequestFilters: InstanceWrapper<
|
||||
ExceptionFilter
|
||||
>[] = [];
|
||||
private readonly globalRequestInterceptors: InstanceWrapper<
|
||||
NestInterceptor
|
||||
>[] = [];
|
||||
private readonly globalRequestFilters: InstanceWrapper<ExceptionFilter>[] = [];
|
||||
private readonly globalRequestInterceptors: InstanceWrapper<NestInterceptor>[] = [];
|
||||
private readonly globalRequestGuards: InstanceWrapper<CanActivate>[] = [];
|
||||
|
||||
constructor(private ioAdapter: WebSocketAdapter | null = null) {}
|
||||
|
||||
@@ -38,9 +38,7 @@ export interface ExternalContextOptions {
|
||||
export class ExternalContextCreator {
|
||||
private readonly contextUtils = new ContextUtils();
|
||||
private readonly externalErrorProxy = new ExternalErrorProxy();
|
||||
private readonly handlerMetadataStorage = new HandlerMetadataStorage<
|
||||
ExternalHandlerMetadata
|
||||
>();
|
||||
private readonly handlerMetadataStorage = new HandlerMetadataStorage<ExternalHandlerMetadata>();
|
||||
private container: NestContainer;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -13,18 +13,17 @@ export class ModuleCompiler {
|
||||
public async compile(
|
||||
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
|
||||
): Promise<ModuleFactory> {
|
||||
const { type, dynamicMetadata } = await this.extractMetadata(metatype);
|
||||
const { type, dynamicMetadata } = this.extractMetadata(await metatype);
|
||||
const token = this.moduleTokenFactory.create(type, dynamicMetadata);
|
||||
return { type, dynamicMetadata, token };
|
||||
}
|
||||
|
||||
public async extractMetadata(
|
||||
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
|
||||
): Promise<{
|
||||
public extractMetadata(
|
||||
metatype: Type<any> | DynamicModule,
|
||||
): {
|
||||
type: Type<any>;
|
||||
dynamicMetadata?: Partial<DynamicModule> | undefined;
|
||||
}> {
|
||||
metatype = await metatype;
|
||||
} {
|
||||
if (!this.isDynamicModule(metatype)) {
|
||||
return { type: metatype };
|
||||
}
|
||||
|
||||
@@ -69,82 +69,6 @@ export interface InjectorDependencyContext {
|
||||
}
|
||||
|
||||
export class Injector {
|
||||
public async loadMiddleware(
|
||||
wrapper: InstanceWrapper,
|
||||
collection: Map<string, InstanceWrapper>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const { metatype } = wrapper;
|
||||
const targetWrapper = collection.get(metatype.name);
|
||||
if (!isUndefined(targetWrapper.instance)) {
|
||||
return;
|
||||
}
|
||||
const loadInstance = (instances: any[]) => {
|
||||
targetWrapper.instance = targetWrapper.isDependencyTreeStatic()
|
||||
? new (metatype as Type<any>)(...instances)
|
||||
: Object.create(metatype.prototype);
|
||||
};
|
||||
await this.resolveConstructorParams(
|
||||
wrapper,
|
||||
moduleRef,
|
||||
null,
|
||||
loadInstance,
|
||||
contextId,
|
||||
inquirer,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadController(
|
||||
wrapper: InstanceWrapper<Controller>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
) {
|
||||
const controllers = moduleRef.controllers;
|
||||
await this.loadInstance<Controller>(
|
||||
wrapper,
|
||||
controllers,
|
||||
moduleRef,
|
||||
contextId,
|
||||
wrapper,
|
||||
);
|
||||
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
|
||||
}
|
||||
|
||||
public async loadInjectable<T = any>(
|
||||
wrapper: InstanceWrapper<T>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const injectables = moduleRef.injectables;
|
||||
await this.loadInstance<T>(
|
||||
wrapper,
|
||||
injectables,
|
||||
moduleRef,
|
||||
contextId,
|
||||
inquirer,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadProvider(
|
||||
wrapper: InstanceWrapper<Injectable>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const providers = moduleRef.providers;
|
||||
await this.loadInstance<Injectable>(
|
||||
wrapper,
|
||||
providers,
|
||||
moduleRef,
|
||||
contextId,
|
||||
inquirer,
|
||||
);
|
||||
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
|
||||
}
|
||||
|
||||
public loadPrototype<T>(
|
||||
{ name }: InstanceWrapper<T>,
|
||||
collection: Map<string, InstanceWrapper<T>>,
|
||||
@@ -164,15 +88,6 @@ export class Injector {
|
||||
}
|
||||
}
|
||||
|
||||
public applyDoneHook<T>(wrapper: InstancePerContext<T>): () => void {
|
||||
let done: () => void;
|
||||
wrapper.donePromise = new Promise<void>((resolve, reject) => {
|
||||
done = resolve;
|
||||
});
|
||||
wrapper.isPending = true;
|
||||
return done;
|
||||
}
|
||||
|
||||
public async loadInstance<T>(
|
||||
wrapper: InstanceWrapper<T>,
|
||||
collection: Map<string, InstanceWrapper>,
|
||||
@@ -225,11 +140,91 @@ export class Injector {
|
||||
);
|
||||
}
|
||||
|
||||
public async loadMiddleware(
|
||||
wrapper: InstanceWrapper,
|
||||
collection: Map<string, InstanceWrapper>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const { metatype } = wrapper;
|
||||
const targetWrapper = collection.get(metatype.name);
|
||||
if (!isUndefined(targetWrapper.instance)) {
|
||||
return;
|
||||
}
|
||||
targetWrapper.instance = Object.create(metatype.prototype);
|
||||
await this.loadInstance(
|
||||
wrapper,
|
||||
collection,
|
||||
moduleRef,
|
||||
contextId,
|
||||
inquirer || wrapper,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadController(
|
||||
wrapper: InstanceWrapper<Controller>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
) {
|
||||
const controllers = moduleRef.controllers;
|
||||
await this.loadInstance<Controller>(
|
||||
wrapper,
|
||||
controllers,
|
||||
moduleRef,
|
||||
contextId,
|
||||
wrapper,
|
||||
);
|
||||
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
|
||||
}
|
||||
|
||||
public async loadInjectable<T = any>(
|
||||
wrapper: InstanceWrapper<T>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const injectables = moduleRef.injectables;
|
||||
await this.loadInstance<T>(
|
||||
wrapper,
|
||||
injectables,
|
||||
moduleRef,
|
||||
contextId,
|
||||
inquirer,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadProvider(
|
||||
wrapper: InstanceWrapper<Injectable>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const providers = moduleRef.providers;
|
||||
await this.loadInstance<Injectable>(
|
||||
wrapper,
|
||||
providers,
|
||||
moduleRef,
|
||||
contextId,
|
||||
inquirer,
|
||||
);
|
||||
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
|
||||
}
|
||||
|
||||
public applyDoneHook<T>(wrapper: InstancePerContext<T>): () => void {
|
||||
let done: () => void;
|
||||
wrapper.donePromise = new Promise<void>((resolve, reject) => {
|
||||
done = resolve;
|
||||
});
|
||||
wrapper.isPending = true;
|
||||
return done;
|
||||
}
|
||||
|
||||
public async resolveConstructorParams<T>(
|
||||
wrapper: InstanceWrapper<T>,
|
||||
moduleRef: Module,
|
||||
inject: InjectorDependency[],
|
||||
callback: (args: unknown[]) => void,
|
||||
callback: (args: unknown[]) => void | Promise<void>,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
parentInquirer?: InstanceWrapper,
|
||||
@@ -274,7 +269,7 @@ export class Injector {
|
||||
if (!instanceHost.isResolved && !paramWrapper.forwardRef) {
|
||||
isResolved = false;
|
||||
}
|
||||
return instanceHost && instanceHost.instance;
|
||||
return instanceHost?.instance;
|
||||
} catch (err) {
|
||||
const isOptional = optionalDependenciesIds.includes(index);
|
||||
if (!isOptional) {
|
||||
@@ -372,7 +367,7 @@ export class Injector {
|
||||
|
||||
public async resolveComponentHost<T>(
|
||||
moduleRef: Module,
|
||||
instanceWrapper: InstanceWrapper<T>,
|
||||
instanceWrapper: InstanceWrapper<T | Promise<T>>,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
): Promise<InstanceWrapper> {
|
||||
|
||||
@@ -47,11 +47,15 @@ export class ModuleTokenFactory {
|
||||
|
||||
private replacer(key: string, value: any) {
|
||||
if (typeof value === 'function') {
|
||||
const isClass = /^class\s/.test(Function.prototype.toString.call(value));
|
||||
const funcAsString = value.toString();
|
||||
const isClass = /^class\s/.test(funcAsString);
|
||||
if (isClass) {
|
||||
return value.name;
|
||||
}
|
||||
return hash(value.toString(), { ignoreUnknown: true });
|
||||
return hash(funcAsString, { ignoreUnknown: true });
|
||||
}
|
||||
if (typeof value === 'symbol') {
|
||||
return value.toString();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -168,6 +168,9 @@ export class MiddlewareModule {
|
||||
if (isUndefined(instanceWrapper)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
if (instanceWrapper.isTransient) {
|
||||
return;
|
||||
}
|
||||
await this.bindHandler(
|
||||
instanceWrapper,
|
||||
applicationRef,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
LogLevel,
|
||||
ShutdownSignal,
|
||||
} from '@nestjs/common';
|
||||
import { Abstract, Scope } from '@nestjs/common/interfaces';
|
||||
import { Abstract, DynamicModule, Scope } from '@nestjs/common/interfaces';
|
||||
import { Type } from '@nestjs/common/interfaces/type.interface';
|
||||
import { isEmpty } from '@nestjs/common/utils/shared.utils';
|
||||
import { iterate } from 'iterare';
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
callModuleInitHook,
|
||||
} from './hooks';
|
||||
import { ContextId } from './injector';
|
||||
import { ModuleCompiler } from './injector/compiler';
|
||||
import { NestContainer } from './injector/container';
|
||||
import { Injector } from './injector/injector';
|
||||
import { InstanceLinksHost } from './injector/instance-links-host';
|
||||
@@ -33,8 +34,10 @@ import { Module } from './injector/module';
|
||||
export class NestApplicationContext implements INestApplicationContext {
|
||||
protected isInitialized = false;
|
||||
protected readonly injector = new Injector();
|
||||
private shutdownCleanupRef?: (...args: unknown[]) => unknown;
|
||||
|
||||
private readonly activeShutdownSignals = new Array<string>();
|
||||
private readonly moduleCompiler = new ModuleCompiler();
|
||||
private shutdownCleanupRef?: (...args: unknown[]) => unknown;
|
||||
private _instanceLinksHost: InstanceLinksHost;
|
||||
|
||||
private get instanceLinksHost() {
|
||||
@@ -55,14 +58,20 @@ export class NestApplicationContext implements INestApplicationContext {
|
||||
this.contextModule = modules.next().value;
|
||||
}
|
||||
|
||||
public select<T>(moduleType: Type<T>): INestApplicationContext {
|
||||
const modules = this.container.getModules();
|
||||
const moduleMetatype = this.contextModule.metatype;
|
||||
const scope = this.scope.concat(moduleMetatype);
|
||||
const moduleTokenFactory = this.container.getModuleTokenFactory();
|
||||
public select<T>(
|
||||
moduleType: Type<T> | DynamicModule,
|
||||
): INestApplicationContext {
|
||||
const modulesContainer = this.container.getModules();
|
||||
const contextModuleCtor = this.contextModule.metatype;
|
||||
const scope = this.scope.concat(contextModuleCtor);
|
||||
|
||||
const token = moduleTokenFactory.create(moduleType);
|
||||
const selectedModule = modules.get(token);
|
||||
const moduleTokenFactory = this.container.getModuleTokenFactory();
|
||||
const { type, dynamicMetadata } = this.moduleCompiler.extractMetadata(
|
||||
moduleType,
|
||||
);
|
||||
const token = moduleTokenFactory.create(type, dynamicMetadata);
|
||||
|
||||
const selectedModule = modulesContainer.get(token);
|
||||
if (!selectedModule) {
|
||||
throw new UnknownModuleException();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
PipeTransform,
|
||||
WebSocketAdapter,
|
||||
} from '@nestjs/common';
|
||||
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
@@ -102,11 +105,15 @@ export class NestApplication
|
||||
if (!this.appOptions || !this.appOptions.cors) {
|
||||
return undefined;
|
||||
}
|
||||
const isCorsOptionsObj = isObject(this.appOptions.cors);
|
||||
if (!isCorsOptionsObj) {
|
||||
const passCustomOptions =
|
||||
isObject(this.appOptions.cors) ||
|
||||
typeof this.appOptions.cors === 'function';
|
||||
if (!passCustomOptions) {
|
||||
return this.enableCors();
|
||||
}
|
||||
return this.enableCors(this.appOptions.cors as CorsOptions);
|
||||
return this.enableCors(
|
||||
this.appOptions.cors as CorsOptions | CorsOptionsDelegate<any>,
|
||||
);
|
||||
}
|
||||
|
||||
public createServer<T = any>(): T {
|
||||
@@ -224,7 +231,7 @@ export class NestApplication
|
||||
return this;
|
||||
}
|
||||
|
||||
public enableCors(options?: CorsOptions): void {
|
||||
public enableCors(options?: CorsOptions | CorsOptionsDelegate<any>): void {
|
||||
this.httpAdapter.enableCors(options);
|
||||
}
|
||||
|
||||
@@ -348,7 +355,7 @@ export class NestApplication
|
||||
}
|
||||
|
||||
private listenToPromise(microservice: INestMicroservice) {
|
||||
return new Promise(async resolve => {
|
||||
return new Promise<void>(async resolve => {
|
||||
await microservice.listen(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,10 +215,10 @@ export class NestFactoryStatic {
|
||||
}
|
||||
|
||||
private applyLogger(options: NestApplicationContextOptions | undefined) {
|
||||
if (!options) {
|
||||
if (!options || options?.logger === true || isNil(options?.logger)) {
|
||||
return;
|
||||
}
|
||||
!isNil(options.logger) && Logger.overrideLogger(options.logger);
|
||||
Logger.overrideLogger(options.logger);
|
||||
}
|
||||
|
||||
private createHttpAdapter<T = any>(httpServer?: T): AbstractHttpAdapter {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/core",
|
||||
"version": "7.5.5",
|
||||
"version": "7.6.10",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -30,17 +30,31 @@
|
||||
"@nuxtjs/opencollective": "0.3.2",
|
||||
"fast-safe-stringify": "2.0.7",
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "2.0.3",
|
||||
"object-hash": "2.1.1",
|
||||
"path-to-regexp": "3.2.0",
|
||||
"tslib": "2.0.3",
|
||||
"uuid": "8.3.1"
|
||||
"tslib": "2.1.0",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "7.5.5"
|
||||
"@nestjs/common": "7.6.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.0.0",
|
||||
"@nestjs/microservices": "^7.0.0",
|
||||
"@nestjs/platform-express": "^7.0.0",
|
||||
"@nestjs/websockets": "^7.0.0",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/websockets": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/microservices": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/platform-express": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export class RouterExplorer {
|
||||
module: string,
|
||||
applicationRef: T,
|
||||
basePath: string,
|
||||
host: string,
|
||||
host: string | string[],
|
||||
) {
|
||||
const { instance } = instanceWrapper;
|
||||
const routerPaths = this.scanForPaths(instance);
|
||||
@@ -150,7 +150,7 @@ export class RouterExplorer {
|
||||
instanceWrapper: InstanceWrapper,
|
||||
moduleKey: string,
|
||||
basePath: string,
|
||||
host: string,
|
||||
host: string | string[],
|
||||
) {
|
||||
(routePaths || []).forEach(pathProperties => {
|
||||
const { path, requestMethod } = pathProperties;
|
||||
@@ -179,7 +179,7 @@ export class RouterExplorer {
|
||||
instanceWrapper: InstanceWrapper,
|
||||
moduleKey: string,
|
||||
basePath: string,
|
||||
host: string,
|
||||
host: string | string[],
|
||||
) {
|
||||
const {
|
||||
path: paths,
|
||||
@@ -216,14 +216,24 @@ export class RouterExplorer {
|
||||
});
|
||||
}
|
||||
|
||||
private applyHostFilter(host: string, handler: Function) {
|
||||
private applyHostFilter(host: string | string[], handler: Function) {
|
||||
if (!host) {
|
||||
return handler;
|
||||
}
|
||||
|
||||
const httpAdapterRef = this.container.getHttpAdapterRef();
|
||||
const keys = [];
|
||||
const re = pathToRegexp(host, keys);
|
||||
const hosts = Array.isArray(host) ? host : [host];
|
||||
const hostRegExps = hosts.map((host: string) => {
|
||||
const keys = [];
|
||||
const regexp = pathToRegexp(host, keys);
|
||||
return { regexp, keys };
|
||||
});
|
||||
|
||||
const unsupportedFilteringErrorMessage = Array.isArray(host)
|
||||
? `HTTP adapter does not support filtering on hosts: ["${host.join(
|
||||
'", "',
|
||||
)}"]`
|
||||
: `HTTP adapter does not support filtering on host: "${host}"`;
|
||||
|
||||
return <TRequest extends Record<string, any> = any, TResponse = any>(
|
||||
req: TRequest,
|
||||
@@ -232,14 +242,17 @@ export class RouterExplorer {
|
||||
) => {
|
||||
(req as Record<string, any>).hosts = {};
|
||||
const hostname = httpAdapterRef.getRequestHostname(req) || '';
|
||||
const match = hostname.match(re);
|
||||
if (match) {
|
||||
keys.forEach((key, i) => (req.hosts[key.name] = match[i + 1]));
|
||||
return handler(req, res, next);
|
||||
|
||||
for (const exp of hostRegExps) {
|
||||
const match = hostname.match(exp.regexp);
|
||||
if (match) {
|
||||
exp.keys.forEach((key, i) => (req.hosts[key.name] = match[i + 1]));
|
||||
return handler(req, res, next);
|
||||
}
|
||||
}
|
||||
if (!next) {
|
||||
throw new InternalServerErrorException(
|
||||
`HTTP adapter does not support filtering on host: "${host}"`,
|
||||
unsupportedFilteringErrorMessage,
|
||||
);
|
||||
}
|
||||
return next();
|
||||
|
||||
@@ -132,7 +132,7 @@ export class RoutesResolver implements Resolver {
|
||||
|
||||
private getHostMetadata(
|
||||
metatype: Type<unknown> | Function,
|
||||
): string | undefined {
|
||||
): string | string[] | undefined {
|
||||
return Reflect.getMetadata(HOST_METADATA, metatype);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,28 +189,28 @@ describe('Injector', () => {
|
||||
});
|
||||
|
||||
describe('loadMiddleware', () => {
|
||||
let resolveConstructorParams: sinon.SinonSpy;
|
||||
let loadInstanceSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
resolveConstructorParams = sinon.spy();
|
||||
injector.resolveConstructorParams = resolveConstructorParams;
|
||||
loadInstanceSpy = sinon.spy();
|
||||
injector.loadInstance = loadInstanceSpy;
|
||||
});
|
||||
|
||||
it('should call "resolveConstructorParams" when instance is not resolved', () => {
|
||||
it('should call "loadInstance" when instance is not resolved', () => {
|
||||
const collection = {
|
||||
get: (...args) => ({}),
|
||||
set: (...args) => {},
|
||||
};
|
||||
|
||||
injector.loadMiddleware(
|
||||
{ metatype: { name: '' } } as any,
|
||||
{ metatype: { name: '', prototype: {} } } as any,
|
||||
collection as any,
|
||||
null,
|
||||
);
|
||||
expect(resolveConstructorParams.called).to.be.true;
|
||||
expect(loadInstanceSpy.called).to.be.true;
|
||||
});
|
||||
|
||||
it('should not call "resolveConstructorParams" when instance is not resolved', () => {
|
||||
it('should not call "loadInstanceSpy" when instance is not resolved', () => {
|
||||
const collection = {
|
||||
get: (...args) => ({
|
||||
instance: {},
|
||||
@@ -223,7 +223,7 @@ describe('Injector', () => {
|
||||
collection as any,
|
||||
null,
|
||||
);
|
||||
expect(resolveConstructorParams.called).to.be.false;
|
||||
expect(loadInstanceSpy.called).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('ModuleTokenFactory', () => {
|
||||
const token = factory.create(type, {
|
||||
providers: [{}],
|
||||
} as any);
|
||||
|
||||
expect(token).to.be.deep.eq(
|
||||
hash({
|
||||
id: moduleId,
|
||||
@@ -62,6 +63,24 @@ describe('ModuleTokenFactory', () => {
|
||||
'{"providers":["Provider"],"exports":["Provider"]}',
|
||||
);
|
||||
});
|
||||
it('should serialize symbols in a dynamic metadata object', () => {
|
||||
const metadata = {
|
||||
providers: [
|
||||
{
|
||||
provide: Symbol('a'),
|
||||
useValue: 'a',
|
||||
},
|
||||
{
|
||||
provide: Symbol('b'),
|
||||
useValue: 'b',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(factory.getDynamicMetadataToken(metadata)).to.be.eql(
|
||||
'{"providers":[{"provide":"Symbol(a)","useValue":"a"},{"provide":"Symbol(b)","useValue":"b"}]}',
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('when metadata does not exist', () => {
|
||||
it('should return empty string', () => {
|
||||
|
||||
@@ -125,7 +125,7 @@ data: hello
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
'Cache-Control':
|
||||
'private, no-cache, no-store, must-revalidate, max-age=0',
|
||||
'private, no-cache, no-store, must-revalidate, max-age=0, no-transform',
|
||||
'Transfer-Encoding': 'identity',
|
||||
Pragma: 'no-cache',
|
||||
Expire: '0',
|
||||
|
||||
@@ -56,16 +56,21 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
|
||||
#### Principal Sponsors
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="180" valign="middle" /></a></td><td>
|
||||
<a href="https://valor-software.com/" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/valor-software.png" width="180" valign="middle" /></a></td>
|
||||
<a href="https://github.com/Sanofi-IADC" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/sanofi.png" width="180" valign="middle" /></a></td>
|
||||
<td>
|
||||
<a href="https://trilon.io" target="_blank"><img src="https://nestjs.com/img/trilon.svg" width="200" valign="middle" /></a></td>
|
||||
</tr></table>
|
||||
|
||||
#### Gold Sponsors
|
||||
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://valor-software.com/" target="_blank"><img src="https://docs.nestjs.com/assets/sponsors/valor-software.png" width="170" valign="middle" /></a></td></tr></table>
|
||||
|
||||
#### Silver Sponsors
|
||||
|
||||
<table style="text-align:center;"><tr><td>
|
||||
<a href="https://neoteric.eu/" target="_blank"><img src="https://nestjs.com/img/neoteric-cut.png" width="120" valign="middle" /></a> </td><td>
|
||||
<a href="http://gojob.com" target="_blank"><img src="http://nestjs.com/img/gojob-logo.png" valign="middle" width="100" /></a> </td><td>
|
||||
<a href="https://trilon.io" target="_blank"><img src="https://nestjs.com/img/trilon.svg" width="170" valign="middle" /></a> </td><td>
|
||||
<a href="http://www.leogistics.com" target="_blank"><img src="https://nestjs.com/img/leogistics-logo.jpeg" width="150" valign="middle" /></td><td>
|
||||
<a href="http://www.meetdandy.com" target="_blank"><img src="https://nestjs.com/img/dandy-wide-logo.png" width="150" valign="middle" /></td></tr></table>
|
||||
|
||||
@@ -90,6 +95,11 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://genuinebee.com/" target="_blank"><img src="https://nestjs.com/img/genuinebee.svg" width="97" valign="middle" /></a> </td>
|
||||
<td align="center" valign="middle"><a href="https://sanyodigital.com/" target="_blank"><img src="https://nestjs.com/img/sanyo-digital.png" width="130" valign="middle" /></a></td></tr><tr><td align="center" valign="middle"><a href="https://vpn-review.com/vpn-for-torrenting" target="_blank"><img src="https://nestjs.com/img/vpn-review-logo.png" width="85" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://lambda-it.ch/" target="_blank"><img src="https://nestjs.com/img/lambda-it-logo.svg" width="115" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://pickwriters.com/top-10-translation-services" target="_blank"><img src="https://nestjs.com/img/pickwriters-logo.png" width="40" valign="middle" /></a></td><td align="center" valign="middle"><a href="https://thewordpoint.com/services/localization" target="_blank"><img src="https://nestjs.com/img/thewordpoint-logo.png" width="40" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://streamat.se/" target="_blank"><img src="https://nestjs.com/img/streamat-logo.png" width="120" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://filmen.nu/" target="_blank"><img src="https://nestjs.com/img/filmen-logo.png" width="120" valign="middle" /></a></td></tr><tr>
|
||||
<td align="center" valign="middle"><a href="https://meercode.io/" target="_blank"><img src="https://nestjs.com/img/meercode-logo.png" width="60" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.najlepszeplatformyforex.pl/blog/broker-xtb/" target="_blank"><img src="https://nestjs.com/img/npf-logo.jpg" width="200" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://thestandarddaily.com/" target="_blank"><img src="https://nestjs.com/img/the-standard-daily-logo.png" width="180" valign="middle" /></a></td>
|
||||
</tr></table>
|
||||
|
||||
## Backers
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from '../constants';
|
||||
import { KafkaResponseDeserializer } from '../deserializers/kafka-response.deserializer';
|
||||
import { KafkaHeaders } from '../enums';
|
||||
import { InvalidKafkaClientTopicPartitionException } from '../errors/invalid-kafka-client-topic-partition.exception';
|
||||
import { InvalidKafkaClientTopicException } from '../errors/invalid-kafka-client-topic.exception';
|
||||
import {
|
||||
BrokersFunction,
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
import {
|
||||
KafkaLogger,
|
||||
KafkaParser,
|
||||
KafkaRoundRobinPartitionAssigner,
|
||||
KafkaReplyPartitionAssigner,
|
||||
} from '../helpers';
|
||||
import {
|
||||
KafkaOptions,
|
||||
@@ -46,7 +45,7 @@ export class ClientKafka extends ClientProxy {
|
||||
protected producer: Producer = null;
|
||||
protected logger = new Logger(ClientKafka.name);
|
||||
protected responsePatterns: string[] = [];
|
||||
protected consumerAssignments: { [key: string]: number[] } = {};
|
||||
protected consumerAssignments: { [key: string]: number } = {};
|
||||
|
||||
protected brokers: string[] | BrokersFunction;
|
||||
protected clientId: string;
|
||||
@@ -59,17 +58,16 @@ export class ClientKafka extends ClientProxy {
|
||||
this.getOptionsProp(this.options, 'client') || ({} as KafkaConfig);
|
||||
const consumerOptions =
|
||||
this.getOptionsProp(this.options, 'consumer') || ({} as ConsumerConfig);
|
||||
const postfixId =
|
||||
this.getOptionsProp(this.options, 'postfixId') || '-client';
|
||||
|
||||
this.brokers = clientOptions.brokers || [KAFKA_DEFAULT_BROKER];
|
||||
|
||||
// Append a unique id to the clientId and groupId
|
||||
// so they don't collide with a microservices client
|
||||
this.clientId =
|
||||
(clientOptions.clientId || KAFKA_DEFAULT_CLIENT) +
|
||||
(clientOptions.clientIdPostfix || '-client');
|
||||
this.groupId =
|
||||
(consumerOptions.groupId || KAFKA_DEFAULT_GROUP) +
|
||||
(clientOptions.clientIdPostfix || '-client');
|
||||
(clientOptions.clientId || KAFKA_DEFAULT_CLIENT) + postfixId;
|
||||
this.groupId = (consumerOptions.groupId || KAFKA_DEFAULT_GROUP) + postfixId;
|
||||
|
||||
kafkaPackage = loadPackage('kafkajs', ClientKafka.name, () =>
|
||||
require('kafkajs'),
|
||||
@@ -99,11 +97,8 @@ export class ClientKafka extends ClientProxy {
|
||||
this.client = this.createClient();
|
||||
|
||||
const partitionAssigners = [
|
||||
(
|
||||
config: ConstructorParameters<
|
||||
typeof KafkaRoundRobinPartitionAssigner
|
||||
>[0],
|
||||
) => new KafkaRoundRobinPartitionAssigner(config),
|
||||
(config: ConstructorParameters<typeof KafkaReplyPartitionAssigner>[1]) =>
|
||||
new KafkaReplyPartitionAssigner(this, config),
|
||||
] as any[];
|
||||
|
||||
const consumerOptions = Object.assign(
|
||||
@@ -188,6 +183,10 @@ export class ClientKafka extends ClientProxy {
|
||||
};
|
||||
}
|
||||
|
||||
public getConsumerAssignments() {
|
||||
return this.consumerAssignments;
|
||||
}
|
||||
|
||||
protected dispatchEvent(packet: OutgoingEvent): Promise<any> {
|
||||
const pattern = this.normalizePattern(packet.pattern);
|
||||
const outgoingEvent = this.serializer.serialize(packet.data);
|
||||
@@ -202,17 +201,13 @@ export class ClientKafka extends ClientProxy {
|
||||
}
|
||||
|
||||
protected getReplyTopicPartition(topic: string): string {
|
||||
const topicAssignments = this.consumerAssignments[topic];
|
||||
if (isUndefined(topicAssignments)) {
|
||||
const minimumPartition = this.consumerAssignments[topic];
|
||||
if (isUndefined(minimumPartition)) {
|
||||
throw new InvalidKafkaClientTopicException(topic);
|
||||
}
|
||||
|
||||
// if the current member isn't listening to
|
||||
// any partitions on the topic then throw an error.
|
||||
if (isUndefined(topicAssignments[0])) {
|
||||
throw new InvalidKafkaClientTopicPartitionException(topic);
|
||||
}
|
||||
return topicAssignments[0].toString();
|
||||
// get the minimum partition
|
||||
return minimumPartition.toString();
|
||||
}
|
||||
|
||||
protected publish(
|
||||
@@ -241,7 +236,7 @@ export class ClientKafka extends ClientProxy {
|
||||
},
|
||||
this.options.send || {},
|
||||
);
|
||||
this.producer.send(message);
|
||||
this.producer.send(message).catch(err => callback({ err }));
|
||||
|
||||
return () => this.routingMap.delete(packet.id);
|
||||
} catch (err) {
|
||||
@@ -254,7 +249,18 @@ export class ClientKafka extends ClientProxy {
|
||||
}
|
||||
|
||||
protected setConsumerAssignments(data: ConsumerGroupJoinEvent): void {
|
||||
this.consumerAssignments = data.payload.memberAssignment;
|
||||
const consumerAssignments: { [key: string]: number } = {};
|
||||
|
||||
// only need to set the minimum
|
||||
Object.keys(data.payload.memberAssignment).forEach(memberId => {
|
||||
const minimumPartition = Math.min(
|
||||
...data.payload.memberAssignment[memberId],
|
||||
);
|
||||
|
||||
consumerAssignments[memberId] = minimumPartition;
|
||||
});
|
||||
|
||||
this.consumerAssignments = consumerAssignments;
|
||||
}
|
||||
|
||||
protected initializeSerializer(options: KafkaOptions['options']) {
|
||||
|
||||
@@ -157,7 +157,7 @@ export class ClientMqtt extends ClientProxy {
|
||||
const pattern = this.normalizePattern(packet.pattern);
|
||||
const serializedPacket = this.serializer.serialize(packet);
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
this.mqttClient.publish(pattern, JSON.stringify(serializedPacket), err =>
|
||||
err ? reject(err) : resolve(),
|
||||
),
|
||||
|
||||
@@ -111,7 +111,7 @@ export class ClientNats extends ClientProxy {
|
||||
const pattern = this.normalizePattern(packet.pattern);
|
||||
const serializedPacket = this.serializer.serialize(packet);
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
this.natsClient.publish(pattern, serializedPacket as any, err =>
|
||||
err ? reject(err) : resolve(),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Transport } from '../enums/transport.enum';
|
||||
import {
|
||||
ClientOptions,
|
||||
CustomClientOptions,
|
||||
TcpClientOptions,
|
||||
} from '../interfaces/client-metadata.interface';
|
||||
import { Closeable } from '../interfaces/closeable.interface';
|
||||
@@ -30,7 +31,16 @@ export class ClientProxyFactory {
|
||||
clientOptions: { transport: Transport.GRPC } & ClientOptions,
|
||||
): ClientGrpcProxy;
|
||||
public static create(clientOptions: ClientOptions): ClientProxy & Closeable;
|
||||
public static create(clientOptions: ClientOptions): ClientProxy & Closeable {
|
||||
public static create(
|
||||
clientOptions: CustomClientOptions,
|
||||
): ClientProxy & Closeable;
|
||||
public static create(
|
||||
clientOptions: ClientOptions | CustomClientOptions,
|
||||
): ClientProxy & Closeable {
|
||||
if (this.isCustomClientOptions(clientOptions)) {
|
||||
const { customClass, options } = clientOptions;
|
||||
return new customClass(options);
|
||||
}
|
||||
const { transport, options } = clientOptions;
|
||||
switch (transport) {
|
||||
case Transport.REDIS:
|
||||
@@ -49,4 +59,10 @@ export class ClientProxyFactory {
|
||||
return new ClientTCP(options as TcpClientOptions['options']);
|
||||
}
|
||||
}
|
||||
|
||||
private static isCustomClientOptions(
|
||||
options: ClientOptions | CustomClientOptions,
|
||||
): options is CustomClientOptions {
|
||||
return !!(options as CustomClientOptions).customClass;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,17 +84,25 @@ export abstract class ClientProxy {
|
||||
): (packet: WritePacket) => void {
|
||||
return ({ err, response, isDisposed }: WritePacket) => {
|
||||
if (err) {
|
||||
return observer.error(err);
|
||||
return observer.error(this.serializeError(err));
|
||||
} else if (response !== undefined && isDisposed) {
|
||||
observer.next(response);
|
||||
observer.next(this.serializeResponse(response));
|
||||
return observer.complete();
|
||||
} else if (isDisposed) {
|
||||
return observer.complete();
|
||||
}
|
||||
observer.next(response);
|
||||
observer.next(this.serializeResponse(response));
|
||||
};
|
||||
}
|
||||
|
||||
protected serializeError(err: any): any {
|
||||
return err;
|
||||
}
|
||||
|
||||
protected serializeResponse(response: any): any {
|
||||
return response;
|
||||
}
|
||||
|
||||
protected assignPacketId(packet: ReadPacket): ReadPacket & PacketId {
|
||||
const id = randomStringGenerator();
|
||||
return Object.assign(packet, { id });
|
||||
|
||||
@@ -190,7 +190,7 @@ export class ClientRedis extends ClientProxy {
|
||||
const pattern = this.normalizePattern(packet.pattern);
|
||||
const serializedPacket = this.serializer.serialize(packet);
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
this.pubClient.publish(pattern, JSON.stringify(serializedPacket), err =>
|
||||
err ? reject(err) : resolve(),
|
||||
),
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
RQM_DEFAULT_QUEUE_OPTIONS,
|
||||
RQM_DEFAULT_URL,
|
||||
} from '../constants';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
import { ReadPacket, RmqOptions, WritePacket } from '../interfaces';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
|
||||
let rqmPackage: any = {};
|
||||
|
||||
@@ -204,14 +204,14 @@ export class ClientRMQ extends ClientProxy {
|
||||
protected dispatchEvent(packet: ReadPacket): Promise<any> {
|
||||
const serializedPacket = this.serializer.serialize(packet);
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
this.channel.sendToQueue(
|
||||
this.queue,
|
||||
Buffer.from(JSON.stringify(serializedPacket)),
|
||||
{
|
||||
persistent: this.persistent,
|
||||
},
|
||||
err => (err ? reject(err) : resolve()),
|
||||
(err: unknown) => (err ? reject(err) : resolve()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,9 +41,7 @@ export interface RpcHandlerMetadata {
|
||||
export class RpcContextCreator {
|
||||
private readonly contextUtils = new ContextUtils();
|
||||
private readonly rpcParamsFactory = new RpcParamsFactory();
|
||||
private readonly handlerMetadataStorage = new HandlerMetadataStorage<
|
||||
RpcHandlerMetadata
|
||||
>();
|
||||
private readonly handlerMetadataStorage = new HandlerMetadataStorage<RpcHandlerMetadata>();
|
||||
|
||||
constructor(
|
||||
private readonly rpcProxy: RpcProxy,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { RuntimeException } from '@nestjs/core/errors/exceptions/runtime.exception';
|
||||
|
||||
export class InvalidKafkaClientTopicPartitionException extends RuntimeException {
|
||||
constructor(topic?: string) {
|
||||
super(
|
||||
`The client consumer subscribed to the topic (${topic}) which is not assigned to any partitions.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,931 +0,0 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as net from 'net';
|
||||
import * as tls from 'tls';
|
||||
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
type XOR<T, U> = T | U extends object
|
||||
? (Without<T, U> & U) | (Without<U, T> & T)
|
||||
: T | U;
|
||||
|
||||
export declare class Kafka {
|
||||
constructor(config: KafkaConfig);
|
||||
producer(config?: ProducerConfig): Producer;
|
||||
consumer(config?: ConsumerConfig): Consumer;
|
||||
admin(config?: AdminConfig): Admin;
|
||||
logger(): Logger;
|
||||
}
|
||||
|
||||
export type BrokersFunction = () => string[] | Promise<string[]>;
|
||||
|
||||
export interface KafkaConfig {
|
||||
brokers: string[] | BrokersFunction;
|
||||
ssl?: tls.ConnectionOptions | boolean;
|
||||
sasl?: SASLOptions;
|
||||
clientId?: string;
|
||||
clientIdPostfix?: string;
|
||||
connectionTimeout?: number;
|
||||
authenticationTimeout?: number;
|
||||
reauthenticationThreshold?: number;
|
||||
requestTimeout?: number;
|
||||
enforceRequestTimeout?: boolean;
|
||||
retry?: RetryOptions;
|
||||
socketFactory?: ISocketFactory;
|
||||
logLevel?: logLevel;
|
||||
logCreator?: logCreator;
|
||||
}
|
||||
|
||||
export type ISocketFactory = (
|
||||
host: string,
|
||||
port: number,
|
||||
ssl: tls.ConnectionOptions,
|
||||
onConnect: () => void,
|
||||
) => net.Socket;
|
||||
|
||||
export type SASLMechanism = 'plain' | 'scram-sha-256' | 'scram-sha-512' | 'aws';
|
||||
|
||||
export interface SASLOptions {
|
||||
mechanism: SASLMechanism;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ProducerConfig {
|
||||
createPartitioner?: ICustomPartitioner;
|
||||
retry?: RetryOptions;
|
||||
metadataMaxAge?: number;
|
||||
allowAutoTopicCreation?: boolean;
|
||||
idempotent?: boolean;
|
||||
transactionalId?: string;
|
||||
transactionTimeout?: number;
|
||||
maxInFlightRequests?: number;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
key?: Buffer | string | null;
|
||||
value: Buffer | string | null;
|
||||
partition?: number;
|
||||
headers?: IHeaders;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface PartitionerArgs {
|
||||
topic: string;
|
||||
partitionMetadata: PartitionMetadata[];
|
||||
message: Message;
|
||||
}
|
||||
|
||||
export type ICustomPartitioner = () => (args: PartitionerArgs) => number;
|
||||
export type DefaultPartitioner = ICustomPartitioner;
|
||||
export type JavaCompatiblePartitioner = ICustomPartitioner;
|
||||
|
||||
export let Partitioners: {
|
||||
DefaultPartitioner: DefaultPartitioner;
|
||||
JavaCompatiblePartitioner: JavaCompatiblePartitioner;
|
||||
};
|
||||
|
||||
export type PartitionMetadata = {
|
||||
partitionErrorCode: number;
|
||||
partitionId: number;
|
||||
leader: number;
|
||||
replicas: number[];
|
||||
isr: number[];
|
||||
offlineReplicas?: number[];
|
||||
};
|
||||
|
||||
export interface IHeaders {
|
||||
[key: string]: Buffer | string;
|
||||
}
|
||||
|
||||
export interface ConsumerConfig {
|
||||
groupId: string;
|
||||
partitionAssigners?: PartitionAssigner[];
|
||||
metadataMaxAge?: number;
|
||||
sessionTimeout?: number;
|
||||
rebalanceTimeout?: number;
|
||||
heartbeatInterval?: number;
|
||||
maxBytesPerPartition?: number;
|
||||
minBytes?: number;
|
||||
maxBytes?: number;
|
||||
maxWaitTimeInMs?: number;
|
||||
retry?: RetryOptions & {
|
||||
restartOnFailure?: (err: Error) => Promise<boolean>;
|
||||
};
|
||||
allowAutoTopicCreation?: boolean;
|
||||
maxInFlightRequests?: number;
|
||||
readUncommitted?: boolean;
|
||||
rackId?: string;
|
||||
}
|
||||
|
||||
export type PartitionAssigner = (config: { cluster: Cluster }) => Assigner;
|
||||
|
||||
export interface CoordinatorMetadata {
|
||||
errorCode: number;
|
||||
coordinator: {
|
||||
nodeId: number;
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type Cluster = {
|
||||
isConnected(): boolean;
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
refreshMetadata(): Promise<void>;
|
||||
refreshMetadataIfNecessary(): Promise<void>;
|
||||
addTargetTopic(topic: string): Promise<void>;
|
||||
findBroker(node: { nodeId: string }): Promise<Broker>;
|
||||
findControllerBroker(): Promise<Broker>;
|
||||
findTopicPartitionMetadata(topic: string): PartitionMetadata[];
|
||||
findLeaderForPartitions(
|
||||
topic: string,
|
||||
partitions: number[],
|
||||
): { [leader: string]: number[] };
|
||||
findGroupCoordinator(group: { groupId: string }): Promise<Broker>;
|
||||
findGroupCoordinatorMetadata(group: {
|
||||
groupId: string;
|
||||
}): Promise<CoordinatorMetadata>;
|
||||
defaultOffset(config: { fromBeginning: boolean }): number;
|
||||
fetchTopicsOffset(
|
||||
topics: Array<
|
||||
{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number }>;
|
||||
} & XOR<{ fromBeginning: boolean }, { fromTimestamp: number }>
|
||||
>,
|
||||
): Promise<{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number; offset: string }>;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type Assignment = { [topic: string]: number[] };
|
||||
|
||||
export type GroupMember = { memberId: string; memberMetadata: Buffer };
|
||||
|
||||
export type GroupMemberAssignment = {
|
||||
memberId: string;
|
||||
memberAssignment: Buffer;
|
||||
};
|
||||
|
||||
export type GroupState = { name: string; metadata: Buffer };
|
||||
|
||||
export type Assigner = {
|
||||
name: string;
|
||||
version: number;
|
||||
assign(group: {
|
||||
members: GroupMember[];
|
||||
topics: string[];
|
||||
}): Promise<GroupMemberAssignment[]>;
|
||||
protocol(subscription: { topics: string[] }): GroupState;
|
||||
};
|
||||
|
||||
export interface RetryOptions {
|
||||
maxRetryTime?: number;
|
||||
initialRetryTime?: number;
|
||||
factor?: number;
|
||||
multiplier?: number;
|
||||
retries?: number;
|
||||
}
|
||||
|
||||
export interface AdminConfig {
|
||||
retry?: RetryOptions;
|
||||
}
|
||||
|
||||
export interface ITopicConfig {
|
||||
topic: string;
|
||||
numPartitions?: number;
|
||||
replicationFactor?: number;
|
||||
replicaAssignment?: object[];
|
||||
configEntries?: object[];
|
||||
}
|
||||
|
||||
export interface ITopicPartitionConfig {
|
||||
topic: string;
|
||||
count: number;
|
||||
assignments?: Array<Array<number>>;
|
||||
}
|
||||
|
||||
export interface ITopicMetadata {
|
||||
name: string;
|
||||
partitions: PartitionMetadata[];
|
||||
}
|
||||
|
||||
export enum ResourceTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
TOPIC = 2,
|
||||
GROUP = 3,
|
||||
CLUSTER = 4,
|
||||
TRANSACTIONAL_ID = 5,
|
||||
DELEGATION_TOKEN = 6,
|
||||
}
|
||||
|
||||
export interface ResourceConfigQuery {
|
||||
type: ResourceTypes;
|
||||
name: string;
|
||||
configNames?: string[];
|
||||
}
|
||||
|
||||
export interface ConfigEntries {
|
||||
configName: string;
|
||||
configValue: string;
|
||||
isDefault: boolean;
|
||||
isSensitive: boolean;
|
||||
readOnly: boolean;
|
||||
configSynonyms: ConfigSynonyms[];
|
||||
}
|
||||
|
||||
export interface ConfigSynonyms {
|
||||
configName: string;
|
||||
configValue: string;
|
||||
configSource: number;
|
||||
}
|
||||
|
||||
export interface DescribeConfigResponse {
|
||||
resources: {
|
||||
configEntries: ConfigEntries[];
|
||||
errorCode: number;
|
||||
errorMessage: string;
|
||||
resourceName: string;
|
||||
resourceType: ResourceTypes;
|
||||
}[];
|
||||
throttleTime: number;
|
||||
}
|
||||
|
||||
export interface IResourceConfig {
|
||||
type: ResourceTypes;
|
||||
name: string;
|
||||
configEntries: { name: string; value: string }[];
|
||||
}
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
||||
export type AdminEvents = {
|
||||
CONNECT: 'admin.connect';
|
||||
DISCONNECT: 'admin.disconnect';
|
||||
REQUEST: 'admin.network.request';
|
||||
REQUEST_TIMEOUT: 'admin.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'admin.network.request_queue_size';
|
||||
};
|
||||
|
||||
export interface InstrumentationEvent<T> {
|
||||
id: string;
|
||||
type: string;
|
||||
timestamp: number;
|
||||
payload: T;
|
||||
}
|
||||
|
||||
export type RemoveInstrumentationEventListener<T> = () => void;
|
||||
|
||||
export type ConnectEvent = InstrumentationEvent<null>;
|
||||
export type DisconnectEvent = InstrumentationEvent<null>;
|
||||
export type RequestEvent = InstrumentationEvent<{
|
||||
apiKey: number;
|
||||
apiName: string;
|
||||
apiVersion: number;
|
||||
broker: string;
|
||||
clientId: string;
|
||||
correlationId: number;
|
||||
createdAt: number;
|
||||
duration: number;
|
||||
pendingDuration: number;
|
||||
sentAt: number;
|
||||
size: number;
|
||||
}>;
|
||||
export type RequestTimeoutEvent = InstrumentationEvent<{
|
||||
apiKey: number;
|
||||
apiName: string;
|
||||
apiVersion: number;
|
||||
broker: string;
|
||||
clientId: string;
|
||||
correlationId: number;
|
||||
createdAt: number;
|
||||
pendingDuration: number;
|
||||
sentAt: number;
|
||||
}>;
|
||||
export type RequestQueueSizeEvent = InstrumentationEvent<{
|
||||
broker: string;
|
||||
clientId: string;
|
||||
queueSize: number;
|
||||
}>;
|
||||
|
||||
export interface SeekEntry {
|
||||
partition: number;
|
||||
offset: string;
|
||||
}
|
||||
|
||||
export type Admin = {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
listTopics(): Promise<string[]>;
|
||||
createTopics(options: {
|
||||
validateOnly?: boolean;
|
||||
waitForLeaders?: boolean;
|
||||
timeout?: number;
|
||||
topics: ITopicConfig[];
|
||||
}): Promise<boolean>;
|
||||
deleteTopics(options: { topics: string[]; timeout?: number }): Promise<void>;
|
||||
createPartitions(options: {
|
||||
validateOnly?: boolean;
|
||||
timeout?: number;
|
||||
topicPartitions: ITopicPartitionConfig[];
|
||||
}): Promise<boolean>;
|
||||
fetchTopicMetadata(options?: {
|
||||
topics: string[];
|
||||
}): Promise<{ topics: Array<ITopicMetadata> }>;
|
||||
fetchOffsets(options: {
|
||||
groupId: string;
|
||||
topic: string;
|
||||
}): Promise<Array<SeekEntry & { metadata: string | null }>>;
|
||||
fetchTopicOffsets(
|
||||
topic: string,
|
||||
): Promise<Array<SeekEntry & { high: string; low: string }>>;
|
||||
fetchTopicOffsetsByTimestamp(
|
||||
topic: string,
|
||||
timestamp?: number,
|
||||
): Promise<Array<SeekEntry>>;
|
||||
describeCluster(): Promise<{
|
||||
brokers: Array<{ nodeId: number; host: string; port: number }>;
|
||||
controller: number | null;
|
||||
clusterId: string;
|
||||
}>;
|
||||
setOffsets(options: {
|
||||
groupId: string;
|
||||
topic: string;
|
||||
partitions: SeekEntry[];
|
||||
}): Promise<void>;
|
||||
resetOffsets(options: {
|
||||
groupId: string;
|
||||
topic: string;
|
||||
earliest: boolean;
|
||||
}): Promise<void>;
|
||||
describeConfigs(configs: {
|
||||
resources: ResourceConfigQuery[];
|
||||
includeSynonyms: boolean;
|
||||
}): Promise<DescribeConfigResponse>;
|
||||
alterConfigs(configs: {
|
||||
validateOnly: boolean;
|
||||
resources: IResourceConfig[];
|
||||
}): Promise<any>;
|
||||
listGroups(): Promise<{ groups: GroupOverview[] }>;
|
||||
deleteGroups(groupIds: string[]): Promise<DeleteGroupsResult[]>;
|
||||
describeGroups(groupIds: string[]): Promise<GroupDescriptions>;
|
||||
logger(): Logger;
|
||||
on(
|
||||
eventName: ValueOf<AdminEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
events: AdminEvents;
|
||||
};
|
||||
|
||||
export let PartitionAssigners: { roundRobin: PartitionAssigner };
|
||||
|
||||
export interface ISerializer<T> {
|
||||
encode(value: T): Buffer;
|
||||
decode(buffer: Buffer): T | null;
|
||||
}
|
||||
|
||||
export type MemberMetadata = {
|
||||
version: number;
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
};
|
||||
|
||||
export type MemberAssignment = {
|
||||
version: number;
|
||||
assignment: Assignment;
|
||||
userData: Buffer;
|
||||
};
|
||||
|
||||
export let AssignerProtocol: {
|
||||
MemberMetadata: ISerializer<MemberMetadata>;
|
||||
MemberAssignment: ISerializer<MemberAssignment>;
|
||||
};
|
||||
|
||||
export enum logLevel {
|
||||
NOTHING = 0,
|
||||
ERROR = 1,
|
||||
WARN = 2,
|
||||
INFO = 4,
|
||||
DEBUG = 5,
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
namespace: string;
|
||||
level: logLevel;
|
||||
label: string;
|
||||
log: LoggerEntryContent;
|
||||
}
|
||||
|
||||
export interface LoggerEntryContent {
|
||||
readonly timestamp: Date;
|
||||
readonly message: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type logCreator = (logLevel: logLevel) => (entry: LogEntry) => void;
|
||||
|
||||
export type Logger = {
|
||||
info: (message: string, extra?: object) => void;
|
||||
error: (message: string, extra?: object) => void;
|
||||
warn: (message: string, extra?: object) => void;
|
||||
debug: (message: string, extra?: object) => void;
|
||||
};
|
||||
|
||||
export type Broker = {
|
||||
isConnected(): boolean;
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
apiVersions(): Promise<{
|
||||
[apiKey: number]: { minVersion: number; maxVersion: number };
|
||||
}>;
|
||||
metadata(
|
||||
topics: string[],
|
||||
): Promise<{
|
||||
brokers: Array<{
|
||||
nodeId: number;
|
||||
host: string;
|
||||
port: number;
|
||||
rack?: string;
|
||||
}>;
|
||||
topicMetadata: Array<{
|
||||
topicErrorCode: number;
|
||||
topic: number;
|
||||
partitionMetadata: PartitionMetadata[];
|
||||
}>;
|
||||
}>;
|
||||
offsetCommit(request: {
|
||||
groupId: string;
|
||||
groupGenerationId: number;
|
||||
memberId: string;
|
||||
retentionTime?: number;
|
||||
topics: Array<{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number; offset: string }>;
|
||||
}>;
|
||||
}): Promise<any>;
|
||||
fetch(request: {
|
||||
replicaId?: number;
|
||||
isolationLevel?: number;
|
||||
maxWaitTime?: number;
|
||||
minBytes?: number;
|
||||
maxBytes?: number;
|
||||
topics: Array<{
|
||||
topic: string;
|
||||
partitions: Array<{
|
||||
partition: number;
|
||||
fetchOffset: string;
|
||||
maxBytes: number;
|
||||
}>;
|
||||
}>;
|
||||
rackId?: string;
|
||||
}): Promise<any>;
|
||||
};
|
||||
|
||||
export type KafkaMessage = {
|
||||
key: Buffer;
|
||||
value: Buffer | null;
|
||||
timestamp: string;
|
||||
size: number;
|
||||
attributes: number;
|
||||
offset: string;
|
||||
headers?: IHeaders;
|
||||
};
|
||||
|
||||
export interface ProducerRecord {
|
||||
topic: string;
|
||||
messages: Message[];
|
||||
acks?: number;
|
||||
timeout?: number;
|
||||
compression?: CompressionTypes;
|
||||
}
|
||||
|
||||
export type RecordMetadata = {
|
||||
topicName: string;
|
||||
partition: number;
|
||||
errorCode: number;
|
||||
offset: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
export interface TopicMessages {
|
||||
topic: string;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface ProducerBatch {
|
||||
acks?: number;
|
||||
timeout?: number;
|
||||
compression?: CompressionTypes;
|
||||
topicMessages?: TopicMessages[];
|
||||
}
|
||||
|
||||
export interface PartitionOffset {
|
||||
partition: number;
|
||||
offset: string;
|
||||
}
|
||||
|
||||
export interface TopicOffsets {
|
||||
topic: string;
|
||||
partitions: PartitionOffset[];
|
||||
}
|
||||
|
||||
export interface Offsets {
|
||||
topics: TopicOffsets[];
|
||||
}
|
||||
|
||||
type Sender = {
|
||||
send(record: ProducerRecord): Promise<RecordMetadata[]>;
|
||||
sendBatch(batch: ProducerBatch): Promise<RecordMetadata[]>;
|
||||
};
|
||||
|
||||
export type ProducerEvents = {
|
||||
CONNECT: 'producer.connect';
|
||||
DISCONNECT: 'producer.disconnect';
|
||||
REQUEST: 'producer.network.request';
|
||||
REQUEST_TIMEOUT: 'producer.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'producer.network.request_queue_size';
|
||||
};
|
||||
|
||||
export type Producer = Sender & {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
isIdempotent(): boolean;
|
||||
events: ProducerEvents;
|
||||
on(
|
||||
eventName: ValueOf<ProducerEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
transaction(): Promise<Transaction>;
|
||||
logger(): Logger;
|
||||
};
|
||||
|
||||
export type Transaction = Sender & {
|
||||
sendOffsets(offsets: Offsets & { consumerGroupId: string }): Promise<void>;
|
||||
commit(): Promise<void>;
|
||||
abort(): Promise<void>;
|
||||
isActive(): boolean;
|
||||
};
|
||||
|
||||
export type ConsumerGroup = {
|
||||
groupId: string;
|
||||
generationId: number;
|
||||
memberId: string;
|
||||
coordinator: Broker;
|
||||
};
|
||||
|
||||
export type MemberDescription = {
|
||||
clientHost: string;
|
||||
clientId: string;
|
||||
memberId: string;
|
||||
memberAssignment: Buffer;
|
||||
memberMetadata: Buffer;
|
||||
};
|
||||
|
||||
export type GroupDescription = {
|
||||
groupId: string;
|
||||
members: MemberDescription[];
|
||||
protocol: string;
|
||||
protocolType: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
export type GroupDescriptions = {
|
||||
groups: GroupDescription[];
|
||||
};
|
||||
|
||||
export type TopicPartitions = { topic: string; partitions: number[] };
|
||||
export type TopicPartitionOffsetAndMetadata = {
|
||||
topic: string;
|
||||
partition: number;
|
||||
offset: string;
|
||||
metadata?: string | null;
|
||||
};
|
||||
|
||||
// TODO: Remove with 2.x
|
||||
export type TopicPartitionOffsetAndMedata = TopicPartitionOffsetAndMetadata;
|
||||
|
||||
export type Batch = {
|
||||
topic: string;
|
||||
partition: number;
|
||||
highWatermark: string;
|
||||
messages: KafkaMessage[];
|
||||
isEmpty(): boolean;
|
||||
firstOffset(): string | null;
|
||||
lastOffset(): string;
|
||||
offsetLag(): string;
|
||||
offsetLagLow(): string;
|
||||
};
|
||||
|
||||
export type GroupOverview = {
|
||||
groupId: string;
|
||||
protocolType: string;
|
||||
};
|
||||
|
||||
export type DeleteGroupsResult = {
|
||||
groupId: string;
|
||||
errorCode?: number;
|
||||
};
|
||||
|
||||
export type ConsumerEvents = {
|
||||
HEARTBEAT: 'consumer.heartbeat';
|
||||
COMMIT_OFFSETS: 'consumer.commit_offsets';
|
||||
GROUP_JOIN: 'consumer.group_join';
|
||||
FETCH_START: 'consumer.fetch_start';
|
||||
FETCH: 'consumer.fetch';
|
||||
START_BATCH_PROCESS: 'consumer.start_batch_process';
|
||||
END_BATCH_PROCESS: 'consumer.end_batch_process';
|
||||
CONNECT: 'consumer.connect';
|
||||
DISCONNECT: 'consumer.disconnect';
|
||||
STOP: 'consumer.stop';
|
||||
CRASH: 'consumer.crash';
|
||||
REQUEST: 'consumer.network.request';
|
||||
REQUEST_TIMEOUT: 'consumer.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'consumer.network.request_queue_size';
|
||||
};
|
||||
export type ConsumerHeartbeatEvent = InstrumentationEvent<{
|
||||
groupId: string;
|
||||
memberId: string;
|
||||
groupGenerationId: number;
|
||||
}>;
|
||||
export type ConsumerCommitOffsetsEvent = InstrumentationEvent<{
|
||||
groupId: string;
|
||||
memberId: string;
|
||||
groupGenerationId: number;
|
||||
topics: {
|
||||
topic: string;
|
||||
partitions: {
|
||||
offset: string;
|
||||
partition: string;
|
||||
}[];
|
||||
}[];
|
||||
}>;
|
||||
export interface IMemberAssignment {
|
||||
[key: string]: number[];
|
||||
}
|
||||
export type ConsumerGroupJoinEvent = InstrumentationEvent<{
|
||||
duration: number;
|
||||
groupId: string;
|
||||
isLeader: boolean;
|
||||
leaderId: string;
|
||||
groupProtocol: string;
|
||||
memberId: string;
|
||||
memberAssignment: IMemberAssignment;
|
||||
}>;
|
||||
export type ConsumerFetchEvent = InstrumentationEvent<{
|
||||
numberOfBatches: number;
|
||||
duration: number;
|
||||
}>;
|
||||
interface IBatchProcessEvent {
|
||||
topic: string;
|
||||
partition: number;
|
||||
highWatermark: string;
|
||||
offsetLag: string;
|
||||
offsetLagLow: string;
|
||||
batchSize: number;
|
||||
firstOffset: string;
|
||||
lastOffset: string;
|
||||
}
|
||||
export type ConsumerStartBatchProcessEvent = InstrumentationEvent<
|
||||
IBatchProcessEvent
|
||||
>;
|
||||
export type ConsumerEndBatchProcessEvent = InstrumentationEvent<
|
||||
IBatchProcessEvent & { duration: number }
|
||||
>;
|
||||
export type ConsumerCrashEvent = InstrumentationEvent<{
|
||||
error: Error;
|
||||
groupId: string;
|
||||
}>;
|
||||
|
||||
export interface OffsetsByTopicPartition {
|
||||
topics: TopicOffsets[];
|
||||
}
|
||||
|
||||
export interface EachMessagePayload {
|
||||
topic: string;
|
||||
partition: number;
|
||||
message: KafkaMessage;
|
||||
}
|
||||
|
||||
export interface EachBatchPayload {
|
||||
batch: Batch;
|
||||
resolveOffset(offset: string): void;
|
||||
heartbeat(): Promise<void>;
|
||||
commitOffsetsIfNecessary(offsets?: Offsets): Promise<void>;
|
||||
uncommittedOffsets(): OffsetsByTopicPartition;
|
||||
isRunning(): boolean;
|
||||
isStale(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type alias to keep compatibility with @types/kafkajs
|
||||
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/712ad9d59ccca6a3cc92f347fea0d1c7b02f5eeb/types/kafkajs/index.d.ts#L321-L325
|
||||
*/
|
||||
export type ConsumerEachMessagePayload = EachMessagePayload;
|
||||
|
||||
/**
|
||||
* Type alias to keep compatibility with @types/kafkajs
|
||||
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/712ad9d59ccca6a3cc92f347fea0d1c7b02f5eeb/types/kafkajs/index.d.ts#L327-L336
|
||||
*/
|
||||
export type ConsumerEachBatchPayload = EachBatchPayload;
|
||||
|
||||
export type ConsumerRunConfig = {
|
||||
autoCommit?: boolean;
|
||||
autoCommitInterval?: number | null;
|
||||
autoCommitThreshold?: number | null;
|
||||
eachBatchAutoResolve?: boolean;
|
||||
partitionsConsumedConcurrently?: number;
|
||||
eachBatch?: (payload: EachBatchPayload) => Promise<void>;
|
||||
eachMessage?: (payload: EachMessagePayload) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ConsumerSubscribeTopic = {
|
||||
topic: string | RegExp;
|
||||
fromBeginning?: boolean;
|
||||
};
|
||||
|
||||
export type Consumer = {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
subscribe(topic: ConsumerSubscribeTopic): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
run(config?: ConsumerRunConfig): Promise<void>;
|
||||
commitOffsets(
|
||||
topicPartitions: Array<TopicPartitionOffsetAndMetadata>,
|
||||
): Promise<void>;
|
||||
seek(topicPartition: {
|
||||
topic: string;
|
||||
partition: number;
|
||||
offset: string;
|
||||
}): void;
|
||||
describeGroup(): Promise<GroupDescription>;
|
||||
pause(topics: Array<{ topic: string; partitions?: number[] }>): void;
|
||||
paused(): TopicPartitions[];
|
||||
resume(topics: Array<{ topic: string; partitions?: number[] }>): void;
|
||||
on(
|
||||
eventName: ValueOf<ConsumerEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
logger(): Logger;
|
||||
events: ConsumerEvents;
|
||||
};
|
||||
|
||||
export enum CompressionTypes {
|
||||
None = 0,
|
||||
GZIP = 1,
|
||||
Snappy = 2,
|
||||
LZ4 = 3,
|
||||
ZSTD = 4,
|
||||
}
|
||||
|
||||
export let CompressionCodecs: {
|
||||
[CompressionTypes.GZIP]: () => any;
|
||||
[CompressionTypes.Snappy]: () => any;
|
||||
[CompressionTypes.LZ4]: () => any;
|
||||
[CompressionTypes.ZSTD]: () => any;
|
||||
};
|
||||
|
||||
export declare class KafkaJSError extends Error {
|
||||
constructor(e: Error | string, metadata?: KafkaJSErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSNonRetriableError extends KafkaJSError {
|
||||
constructor(e: Error | string);
|
||||
}
|
||||
|
||||
export declare class KafkaJSProtocolError extends KafkaJSError {
|
||||
constructor(e: Error | string);
|
||||
}
|
||||
|
||||
export declare class KafkaJSOffsetOutOfRange extends KafkaJSProtocolError {
|
||||
constructor(e: Error | string, metadata?: KafkaJSOffsetOutOfRangeMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSNumberOfRetriesExceeded extends KafkaJSNonRetriableError {
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSNumberOfRetriesExceededMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSConnectionError extends KafkaJSError {
|
||||
constructor(e: Error | string, metadata?: KafkaJSConnectionErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSRequestTimeoutError extends KafkaJSError {
|
||||
constructor(e: Error | string, metadata?: KafkaJSRequestTimeoutErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSMetadataNotLoaded extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSTopicMetadataNotLoaded extends KafkaJSMetadataNotLoaded {
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSTopicMetadataNotLoadedMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSStaleTopicMetadataAssignment extends KafkaJSError {
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSStaleTopicMetadataAssignmentMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSServerDoesNotSupportApiKey extends KafkaJSNonRetriableError {
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSServerDoesNotSupportApiKeyMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSBrokerNotFound extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSPartialMessageError extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSSASLAuthenticationError extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSGroupCoordinatorNotFound extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSNotImplemented extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSTimeout extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSLockTimeout extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSUnsupportedMagicByteInMessageSet extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSDeleteGroupsError extends KafkaJSError {
|
||||
constructor(e: Error | string, groups?: KafkaJSDeleteGroupsErrorGroups[]);
|
||||
}
|
||||
|
||||
export interface KafkaJSDeleteGroupsErrorGroups {
|
||||
groupId: string;
|
||||
errorCode: number;
|
||||
error: KafkaJSError;
|
||||
}
|
||||
|
||||
export interface KafkaJSErrorMetadata {
|
||||
retriable?: boolean;
|
||||
topic?: string;
|
||||
partitionId?: number;
|
||||
metadata?: PartitionMetadata;
|
||||
}
|
||||
|
||||
export interface KafkaJSOffsetOutOfRangeMetadata {
|
||||
topic: string;
|
||||
partition: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSNumberOfRetriesExceededMetadata {
|
||||
retryCount: number;
|
||||
retryTime: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSConnectionErrorMetadata {
|
||||
broker?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export interface KafkaJSRequestTimeoutErrorMetadata {
|
||||
broker: string;
|
||||
clientId: string;
|
||||
correlationId: number;
|
||||
createdAt: number;
|
||||
sentAt: number;
|
||||
pendingDuration: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSTopicMetadataNotLoadedMetadata {
|
||||
topic: string;
|
||||
}
|
||||
|
||||
export interface KafkaJSStaleTopicMetadataAssignmentMetadata {
|
||||
topic: string;
|
||||
unknownPartitions: PartitionMetadata[];
|
||||
}
|
||||
|
||||
export interface KafkaJSServerDoesNotSupportApiKeyMetadata {
|
||||
apiKey: number;
|
||||
apiName: string;
|
||||
}
|
||||
672
packages/microservices/external/kafka.interface.ts
vendored
672
packages/microservices/external/kafka.interface.ts
vendored
@@ -1,9 +1,19 @@
|
||||
/**
|
||||
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/kafkajs/index.d.ts
|
||||
* Do NOT add NestJS logic to this interface. It is meant to ONLY represent the types for the kafkajs package.
|
||||
*
|
||||
* @see https://github.com/tulios/kafkajs/blob/master/types/index.d.ts
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as net from 'net';
|
||||
import * as tls from 'tls';
|
||||
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
type XOR<T, U> = T | U extends object
|
||||
? (Without<T, U> & U) | (Without<U, T> & T)
|
||||
: T | U;
|
||||
|
||||
export declare class Kafka {
|
||||
constructor(config: KafkaConfig);
|
||||
producer(config?: ProducerConfig): Producer;
|
||||
@@ -19,7 +29,6 @@ export interface KafkaConfig {
|
||||
ssl?: tls.ConnectionOptions | boolean;
|
||||
sasl?: SASLOptions;
|
||||
clientId?: string;
|
||||
clientIdPostfix?: string;
|
||||
connectionTimeout?: number;
|
||||
authenticationTimeout?: number;
|
||||
reauthenticationThreshold?: number;
|
||||
@@ -31,19 +40,40 @@ export interface KafkaConfig {
|
||||
logCreator?: logCreator;
|
||||
}
|
||||
|
||||
export type ISocketFactory = (
|
||||
host: string,
|
||||
port: number,
|
||||
ssl: tls.ConnectionOptions,
|
||||
onConnect: () => void,
|
||||
) => net.Socket;
|
||||
|
||||
export interface SASLOptions {
|
||||
mechanism: 'plain' | 'scram-sha-256' | 'scram-sha-512' | 'aws';
|
||||
username: string;
|
||||
password: string;
|
||||
export interface ISocketFactoryArgs {
|
||||
host: string;
|
||||
port: number;
|
||||
ssl: tls.ConnectionOptions;
|
||||
onConnect: () => void;
|
||||
}
|
||||
|
||||
export type ISocketFactory = (args: ISocketFactoryArgs) => net.Socket;
|
||||
|
||||
export interface OauthbearerProviderResponse {
|
||||
value: string;
|
||||
}
|
||||
|
||||
type SASLMechanismOptionsMap = {
|
||||
plain: { username: string; password: string };
|
||||
'scram-sha-256': { username: string; password: string };
|
||||
'scram-sha-512': { username: string; password: string };
|
||||
aws: {
|
||||
authorizationIdentity: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
sessionToken?: string;
|
||||
};
|
||||
oauthbearer: {
|
||||
oauthBearerProvider: () => Promise<OauthbearerProviderResponse>;
|
||||
};
|
||||
};
|
||||
|
||||
export type SASLMechanism = keyof SASLMechanismOptionsMap;
|
||||
type SASLMechanismOptions<T> = T extends SASLMechanism
|
||||
? { mechanism: T } & SASLMechanismOptionsMap[T]
|
||||
: never;
|
||||
export type SASLOptions = SASLMechanismOptions<SASLMechanism>;
|
||||
|
||||
export interface ProducerConfig {
|
||||
createPartitioner?: ICustomPartitioner;
|
||||
retry?: RetryOptions;
|
||||
@@ -70,24 +100,25 @@ export interface PartitionerArgs {
|
||||
}
|
||||
|
||||
export type ICustomPartitioner = () => (args: PartitionerArgs) => number;
|
||||
export type DefaultPartitioner = (args: PartitionerArgs) => number;
|
||||
export type JavaCompatiblePartitioner = (args: PartitionerArgs) => number;
|
||||
export type DefaultPartitioner = ICustomPartitioner;
|
||||
export type JavaCompatiblePartitioner = ICustomPartitioner;
|
||||
|
||||
export let Partitioners: {
|
||||
DefaultPartitioner: DefaultPartitioner;
|
||||
JavaCompatiblePartitioner: JavaCompatiblePartitioner;
|
||||
};
|
||||
|
||||
export interface PartitionMetadata {
|
||||
export type PartitionMetadata = {
|
||||
partitionErrorCode: number;
|
||||
partitionId: number;
|
||||
leader: number;
|
||||
replicas: number[];
|
||||
isr: number[];
|
||||
}
|
||||
offlineReplicas?: number[];
|
||||
};
|
||||
|
||||
export interface IHeaders {
|
||||
[key: string]: Buffer;
|
||||
[key: string]: Buffer | string | undefined;
|
||||
}
|
||||
|
||||
export interface ConsumerConfig {
|
||||
@@ -101,15 +132,16 @@ export interface ConsumerConfig {
|
||||
minBytes?: number;
|
||||
maxBytes?: number;
|
||||
maxWaitTimeInMs?: number;
|
||||
retry?: RetryOptions;
|
||||
retry?: RetryOptions & {
|
||||
restartOnFailure?: (err: Error) => Promise<boolean>;
|
||||
};
|
||||
allowAutoTopicCreation?: boolean;
|
||||
maxInFlightRequests?: number;
|
||||
readUncommitted?: boolean;
|
||||
rackId?: string;
|
||||
}
|
||||
|
||||
export interface PartitionAssigner {
|
||||
new (config: { cluster: Cluster }): Assigner;
|
||||
}
|
||||
export type PartitionAssigner = (config: { cluster: Cluster }) => Assigner;
|
||||
|
||||
export interface CoordinatorMetadata {
|
||||
errorCode: number;
|
||||
@@ -120,7 +152,7 @@ export interface CoordinatorMetadata {
|
||||
};
|
||||
}
|
||||
|
||||
export interface Cluster {
|
||||
export type Cluster = {
|
||||
isConnected(): boolean;
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
@@ -140,46 +172,38 @@ export interface Cluster {
|
||||
}): Promise<CoordinatorMetadata>;
|
||||
defaultOffset(config: { fromBeginning: boolean }): number;
|
||||
fetchTopicsOffset(
|
||||
topics: Array<{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number }>;
|
||||
fromBeginning: boolean;
|
||||
}>,
|
||||
topics: Array<
|
||||
{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number }>;
|
||||
} & XOR<{ fromBeginning: boolean }, { fromTimestamp: number }>
|
||||
>,
|
||||
): Promise<{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number; offset: string }>;
|
||||
}>;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Assignment {
|
||||
[topic: string]: number[];
|
||||
}
|
||||
export type Assignment = { [topic: string]: number[] };
|
||||
|
||||
export interface GroupMember {
|
||||
memberId: string;
|
||||
memberMetadata: MemberMetadata;
|
||||
}
|
||||
export type GroupMember = { memberId: string; memberMetadata: Buffer };
|
||||
|
||||
export interface GroupMemberAssignment {
|
||||
export type GroupMemberAssignment = {
|
||||
memberId: string;
|
||||
memberAssignment: Buffer;
|
||||
}
|
||||
};
|
||||
|
||||
export interface GroupState {
|
||||
name: string;
|
||||
metadata: Buffer;
|
||||
}
|
||||
export type GroupState = { name: string; metadata: Buffer };
|
||||
|
||||
export interface Assigner {
|
||||
export type Assigner = {
|
||||
name: string;
|
||||
version: number;
|
||||
assign(group: {
|
||||
members: GroupMember[];
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
}): Promise<GroupMemberAssignment[]>;
|
||||
protocol(subscription: { topics: string[]; userData: Buffer }): GroupState;
|
||||
}
|
||||
protocol(subscription: { topics: string[] }): GroupState;
|
||||
};
|
||||
|
||||
export interface RetryOptions {
|
||||
maxRetryTime?: number;
|
||||
@@ -201,12 +225,22 @@ export interface ITopicConfig {
|
||||
configEntries?: object[];
|
||||
}
|
||||
|
||||
export interface ITopicPartitionConfig {
|
||||
topic: string;
|
||||
count: number;
|
||||
assignments?: Array<Array<number>>;
|
||||
}
|
||||
|
||||
export interface ITopicMetadata {
|
||||
name: string;
|
||||
partitions: PartitionMetadata[];
|
||||
}
|
||||
|
||||
export enum ResourceType {
|
||||
/**
|
||||
* @deprecated
|
||||
* Use ConfigResourceTypes or AclResourceTypes
|
||||
*/
|
||||
export enum ResourceTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
TOPIC = 2,
|
||||
@@ -216,10 +250,58 @@ export enum ResourceType {
|
||||
DELEGATION_TOKEN = 6,
|
||||
}
|
||||
|
||||
export enum AclResourceTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
TOPIC = 2,
|
||||
GROUP = 3,
|
||||
CLUSTER = 4,
|
||||
TRANSACTIONAL_ID = 5,
|
||||
DELEGATION_TOKEN = 6,
|
||||
}
|
||||
|
||||
export enum ConfigResourceTypes {
|
||||
UNKNOWN = 0,
|
||||
TOPIC = 2,
|
||||
BROKER = 4,
|
||||
BROKER_LOGGER = 8,
|
||||
}
|
||||
|
||||
export enum AclPermissionTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
DENY = 2,
|
||||
ALLOW = 3,
|
||||
}
|
||||
|
||||
export enum AclOperationTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
ALL = 2,
|
||||
READ = 3,
|
||||
WRITE = 4,
|
||||
CREATE = 5,
|
||||
DELETE = 6,
|
||||
ALTER = 7,
|
||||
DESCRIBE = 8,
|
||||
CLUSTER_ACTION = 9,
|
||||
DESCRIBE_CONFIGS = 10,
|
||||
ALTER_CONFIGS = 11,
|
||||
IDEMPOTENT_WRITE = 12,
|
||||
}
|
||||
|
||||
export enum ResourcePatternTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
MATCH = 2,
|
||||
LITERAL = 3,
|
||||
PREFIXED = 4,
|
||||
}
|
||||
|
||||
export interface ResourceConfigQuery {
|
||||
type: ResourceType;
|
||||
type: ResourceTypes | ConfigResourceTypes;
|
||||
name: string;
|
||||
configNames: string[];
|
||||
configNames?: string[];
|
||||
}
|
||||
|
||||
export interface ConfigEntries {
|
||||
@@ -243,26 +325,26 @@ export interface DescribeConfigResponse {
|
||||
errorCode: number;
|
||||
errorMessage: string;
|
||||
resourceName: string;
|
||||
resourceType: ResourceType;
|
||||
resourceType: ResourceTypes | ConfigResourceTypes;
|
||||
}[];
|
||||
throttleTime: number;
|
||||
}
|
||||
|
||||
export interface IResourceConfig {
|
||||
type: ResourceType;
|
||||
type: ResourceTypes | ConfigResourceTypes;
|
||||
name: string;
|
||||
configEntries: { name: string; value: string }[];
|
||||
}
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
||||
export interface AdminEvents {
|
||||
export type AdminEvents = {
|
||||
CONNECT: 'admin.connect';
|
||||
DISCONNECT: 'admin.disconnect';
|
||||
REQUEST: 'admin.network.request';
|
||||
REQUEST_TIMEOUT: 'admin.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'admin.network.request_queue_size';
|
||||
}
|
||||
};
|
||||
|
||||
export interface InstrumentationEvent<T> {
|
||||
id: string;
|
||||
@@ -271,6 +353,8 @@ export interface InstrumentationEvent<T> {
|
||||
payload: T;
|
||||
}
|
||||
|
||||
export type RemoveInstrumentationEventListener<T> = () => void;
|
||||
|
||||
export type ConnectEvent = InstrumentationEvent<null>;
|
||||
export type DisconnectEvent = InstrumentationEvent<null>;
|
||||
export type RequestEvent = InstrumentationEvent<{
|
||||
@@ -308,9 +392,69 @@ export interface SeekEntry {
|
||||
offset: string;
|
||||
}
|
||||
|
||||
export interface Admin {
|
||||
export interface Acl {
|
||||
principal: string;
|
||||
host: string;
|
||||
operation: AclOperationTypes;
|
||||
permissionType: AclPermissionTypes;
|
||||
}
|
||||
|
||||
export interface AclResource {
|
||||
resourceType: AclResourceTypes;
|
||||
resourceName: string;
|
||||
resourcePatternType: ResourcePatternTypes;
|
||||
}
|
||||
|
||||
export type AclEntry = Acl & AclResource;
|
||||
|
||||
export type DescribeAclResource = AclResource & {
|
||||
acl: Acl[];
|
||||
};
|
||||
|
||||
export interface DescribeAclResponse {
|
||||
throttleTime: number;
|
||||
errorCode: number;
|
||||
errorMessage?: string;
|
||||
resources: DescribeAclResource[];
|
||||
}
|
||||
|
||||
export interface AclFilter {
|
||||
resourceType: AclResourceTypes;
|
||||
resourceName?: string;
|
||||
resourcePatternType: ResourcePatternTypes;
|
||||
principal?: string;
|
||||
host?: string;
|
||||
operation: AclOperationTypes;
|
||||
permissionType: AclPermissionTypes;
|
||||
}
|
||||
|
||||
export interface MatchingAcl {
|
||||
errorCode: number;
|
||||
errorMessage?: string;
|
||||
resourceType: AclResourceTypes;
|
||||
resourceName: string;
|
||||
resourcePatternType: ResourcePatternTypes;
|
||||
principal: string;
|
||||
host: string;
|
||||
operation: AclOperationTypes;
|
||||
permissionType: AclPermissionTypes;
|
||||
}
|
||||
|
||||
export interface DeleteAclFilterResponses {
|
||||
errorCode: number;
|
||||
errorMessage?: string;
|
||||
matchingAcls: MatchingAcl[];
|
||||
}
|
||||
|
||||
export interface DeleteAclResponse {
|
||||
throttleTime: number;
|
||||
filterResponses: DeleteAclFilterResponses[];
|
||||
}
|
||||
|
||||
export type Admin = {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
listTopics(): Promise<string[]>;
|
||||
createTopics(options: {
|
||||
validateOnly?: boolean;
|
||||
waitForLeaders?: boolean;
|
||||
@@ -318,20 +462,31 @@ export interface Admin {
|
||||
topics: ITopicConfig[];
|
||||
}): Promise<boolean>;
|
||||
deleteTopics(options: { topics: string[]; timeout?: number }): Promise<void>;
|
||||
fetchTopicMetadata(options: {
|
||||
createPartitions(options: {
|
||||
validateOnly?: boolean;
|
||||
timeout?: number;
|
||||
topicPartitions: ITopicPartitionConfig[];
|
||||
}): Promise<boolean>;
|
||||
fetchTopicMetadata(options?: {
|
||||
topics: string[];
|
||||
}): Promise<{ topics: Array<ITopicMetadata> }>;
|
||||
fetchOffsets(options: {
|
||||
groupId: string;
|
||||
topic: string;
|
||||
}): Promise<
|
||||
Array<{ partition: number; offset: string; metadata: string | null }>
|
||||
>;
|
||||
resolveOffsets?: boolean;
|
||||
}): Promise<Array<SeekEntry & { metadata: string | null }>>;
|
||||
fetchTopicOffsets(
|
||||
topic: string,
|
||||
): Promise<
|
||||
Array<{ partition: number; offset: string; high: string; low: string }>
|
||||
>;
|
||||
): Promise<Array<SeekEntry & { high: string; low: string }>>;
|
||||
fetchTopicOffsetsByTimestamp(
|
||||
topic: string,
|
||||
timestamp?: number,
|
||||
): Promise<Array<SeekEntry>>;
|
||||
describeCluster(): Promise<{
|
||||
brokers: Array<{ nodeId: number; host: string; port: number }>;
|
||||
controller: number | null;
|
||||
clusterId: string;
|
||||
}>;
|
||||
setOffsets(options: {
|
||||
groupId: string;
|
||||
topic: string;
|
||||
@@ -350,29 +505,42 @@ export interface Admin {
|
||||
validateOnly: boolean;
|
||||
resources: IResourceConfig[];
|
||||
}): Promise<any>;
|
||||
listGroups(): Promise<{ groups: GroupOverview[] }>;
|
||||
deleteGroups(groupIds: string[]): Promise<DeleteGroupsResult[]>;
|
||||
describeGroups(groupIds: string[]): Promise<GroupDescriptions>;
|
||||
describeAcls(options: AclFilter): Promise<DescribeAclResponse>;
|
||||
deleteAcls(options: { filters: AclFilter[] }): Promise<DeleteAclResponse>;
|
||||
createAcls(options: { acl: AclEntry[] }): Promise<boolean>;
|
||||
deleteTopicRecords(options: {
|
||||
topic: string;
|
||||
partitions: SeekEntry[];
|
||||
}): Promise<void>;
|
||||
logger(): Logger;
|
||||
on(eventName: ValueOf<AdminEvents>, listener: (...args: any[]) => void): void;
|
||||
on(
|
||||
eventName: ValueOf<AdminEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
events: AdminEvents;
|
||||
}
|
||||
};
|
||||
|
||||
export let PartitionAssigners: { roundRobin: PartitionAssigner };
|
||||
|
||||
export interface ISerializer<T> {
|
||||
encode(value: T): Buffer;
|
||||
decode(buffer: Buffer): T;
|
||||
decode(buffer: Buffer): T | null;
|
||||
}
|
||||
|
||||
export interface MemberMetadata {
|
||||
export type MemberMetadata = {
|
||||
version: number;
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
}
|
||||
};
|
||||
|
||||
export interface MemberAssignment {
|
||||
export type MemberAssignment = {
|
||||
version: number;
|
||||
assignment: Assignment;
|
||||
userData: Buffer;
|
||||
}
|
||||
};
|
||||
|
||||
export let AssignerProtocol: {
|
||||
MemberMetadata: ISerializer<MemberMetadata>;
|
||||
@@ -400,11 +568,16 @@ export interface LoggerEntryContent {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type Logger = (entry: LogEntry) => void;
|
||||
export type logCreator = (logLevel: logLevel) => (entry: LogEntry) => void;
|
||||
|
||||
export type logCreator = (logLevel: string) => (entry: LogEntry) => void;
|
||||
export type Logger = {
|
||||
info: (message: string, extra?: object) => void;
|
||||
error: (message: string, extra?: object) => void;
|
||||
warn: (message: string, extra?: object) => void;
|
||||
debug: (message: string, extra?: object) => void;
|
||||
};
|
||||
|
||||
export interface Broker {
|
||||
export type Broker = {
|
||||
isConnected(): boolean;
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
@@ -414,7 +587,12 @@ export interface Broker {
|
||||
metadata(
|
||||
topics: string[],
|
||||
): Promise<{
|
||||
brokers: Array<{ nodeId: number; host: string; port: number }>;
|
||||
brokers: Array<{
|
||||
nodeId: number;
|
||||
host: string;
|
||||
port: number;
|
||||
rack?: string;
|
||||
}>;
|
||||
topicMetadata: Array<{
|
||||
topicErrorCode: number;
|
||||
topic: number;
|
||||
@@ -431,17 +609,33 @@ export interface Broker {
|
||||
partitions: Array<{ partition: number; offset: string }>;
|
||||
}>;
|
||||
}): Promise<any>;
|
||||
}
|
||||
fetch(request: {
|
||||
replicaId?: number;
|
||||
isolationLevel?: number;
|
||||
maxWaitTime?: number;
|
||||
minBytes?: number;
|
||||
maxBytes?: number;
|
||||
topics: Array<{
|
||||
topic: string;
|
||||
partitions: Array<{
|
||||
partition: number;
|
||||
fetchOffset: string;
|
||||
maxBytes: number;
|
||||
}>;
|
||||
}>;
|
||||
rackId?: string;
|
||||
}): Promise<any>;
|
||||
};
|
||||
|
||||
export interface KafkaMessage {
|
||||
export type KafkaMessage = {
|
||||
key: Buffer;
|
||||
value: Buffer;
|
||||
value: Buffer | null;
|
||||
timestamp: string;
|
||||
size: number;
|
||||
attributes: number;
|
||||
offset: string;
|
||||
headers?: IHeaders;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ProducerRecord {
|
||||
topic: string;
|
||||
@@ -451,13 +645,16 @@ export interface ProducerRecord {
|
||||
compression?: CompressionTypes;
|
||||
}
|
||||
|
||||
export interface RecordMetadata {
|
||||
export type RecordMetadata = {
|
||||
topicName: string;
|
||||
partition: number;
|
||||
errorCode: number;
|
||||
offset: string;
|
||||
timestamp: string;
|
||||
}
|
||||
offset?: string;
|
||||
timestamp?: string;
|
||||
baseOffset?: string;
|
||||
logAppendTime?: string;
|
||||
logStartOffset?: string;
|
||||
};
|
||||
|
||||
export interface TopicMessages {
|
||||
topic: string;
|
||||
@@ -465,10 +662,10 @@ export interface TopicMessages {
|
||||
}
|
||||
|
||||
export interface ProducerBatch {
|
||||
acks: number;
|
||||
timeout: number;
|
||||
compression: CompressionTypes;
|
||||
topicMessages: TopicMessages[];
|
||||
acks?: number;
|
||||
timeout?: number;
|
||||
compression?: CompressionTypes;
|
||||
topicMessages?: TopicMessages[];
|
||||
}
|
||||
|
||||
export interface PartitionOffset {
|
||||
@@ -485,18 +682,18 @@ export interface Offsets {
|
||||
topics: TopicOffsets[];
|
||||
}
|
||||
|
||||
interface Sender {
|
||||
type Sender = {
|
||||
send(record: ProducerRecord): Promise<RecordMetadata[]>;
|
||||
sendBatch(batch: ProducerBatch): Promise<RecordMetadata[]>;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ProducerEvents {
|
||||
export type ProducerEvents = {
|
||||
CONNECT: 'producer.connect';
|
||||
DISCONNECT: 'producer.disconnect';
|
||||
REQUEST: 'producer.network.request';
|
||||
REQUEST_TIMEOUT: 'producer.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'producer.network.request_queue_size';
|
||||
}
|
||||
};
|
||||
|
||||
export type Producer = Sender & {
|
||||
connect(): Promise<void>;
|
||||
@@ -506,7 +703,7 @@ export type Producer = Sender & {
|
||||
on(
|
||||
eventName: ValueOf<ProducerEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): void;
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
transaction(): Promise<Transaction>;
|
||||
logger(): Logger;
|
||||
};
|
||||
@@ -518,41 +715,54 @@ export type Transaction = Sender & {
|
||||
isActive(): boolean;
|
||||
};
|
||||
|
||||
export interface ConsumerGroup {
|
||||
export type ConsumerGroup = {
|
||||
groupId: string;
|
||||
generationId: number;
|
||||
memberId: string;
|
||||
coordinator: Broker;
|
||||
}
|
||||
};
|
||||
|
||||
export interface MemberDescription {
|
||||
export type MemberDescription = {
|
||||
clientHost: string;
|
||||
clientId: string;
|
||||
memberId: string;
|
||||
memberAssignment: Buffer;
|
||||
memberMetadata: Buffer;
|
||||
}
|
||||
};
|
||||
|
||||
export interface GroupDescription {
|
||||
// See https://github.com/apache/kafka/blob/2.4.0/clients/src/main/java/org/apache/kafka/common/ConsumerGroupState.java#L25
|
||||
export type ConsumerGroupState =
|
||||
| 'Unknown'
|
||||
| 'PreparingRebalance'
|
||||
| 'CompletingRebalance'
|
||||
| 'Stable'
|
||||
| 'Dead'
|
||||
| 'Empty';
|
||||
|
||||
export type GroupDescription = {
|
||||
groupId: string;
|
||||
members: MemberDescription[];
|
||||
protocol: string;
|
||||
protocolType: string;
|
||||
state: string;
|
||||
}
|
||||
state: ConsumerGroupState;
|
||||
};
|
||||
|
||||
export interface TopicPartitions {
|
||||
topic: string;
|
||||
partitions: number[];
|
||||
}
|
||||
export interface TopicPartitionOffsetAndMedata {
|
||||
export type GroupDescriptions = {
|
||||
groups: GroupDescription[];
|
||||
};
|
||||
|
||||
export type TopicPartitions = { topic: string; partitions: number[] };
|
||||
export type TopicPartitionOffsetAndMetadata = {
|
||||
topic: string;
|
||||
partition: number;
|
||||
offset: string;
|
||||
metadata?: string | null;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Batch {
|
||||
// TODO: Remove with 2.x
|
||||
export type TopicPartitionOffsetAndMedata = TopicPartitionOffsetAndMetadata;
|
||||
|
||||
export type Batch = {
|
||||
topic: string;
|
||||
partition: number;
|
||||
highWatermark: string;
|
||||
@@ -562,12 +772,24 @@ export interface Batch {
|
||||
lastOffset(): string;
|
||||
offsetLag(): string;
|
||||
offsetLagLow(): string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ConsumerEvents {
|
||||
export type GroupOverview = {
|
||||
groupId: string;
|
||||
protocolType: string;
|
||||
};
|
||||
|
||||
export type DeleteGroupsResult = {
|
||||
groupId: string;
|
||||
errorCode?: number;
|
||||
error?: KafkaJSProtocolError;
|
||||
};
|
||||
|
||||
export type ConsumerEvents = {
|
||||
HEARTBEAT: 'consumer.heartbeat';
|
||||
COMMIT_OFFSETS: 'consumer.commit_offsets';
|
||||
GROUP_JOIN: 'consumer.group_join';
|
||||
FETCH_START: 'consumer.fetch_start';
|
||||
FETCH: 'consumer.fetch';
|
||||
START_BATCH_PROCESS: 'consumer.start_batch_process';
|
||||
END_BATCH_PROCESS: 'consumer.end_batch_process';
|
||||
@@ -575,10 +797,11 @@ export interface ConsumerEvents {
|
||||
DISCONNECT: 'consumer.disconnect';
|
||||
STOP: 'consumer.stop';
|
||||
CRASH: 'consumer.crash';
|
||||
RECEIVED_UNSUBSCRIBED_TOPICS: 'consumer.received_unsubscribed_topics';
|
||||
REQUEST: 'consumer.network.request';
|
||||
REQUEST_TIMEOUT: 'consumer.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'consumer.network.request_queue_size';
|
||||
}
|
||||
};
|
||||
export type ConsumerHeartbeatEvent = InstrumentationEvent<{
|
||||
groupId: string;
|
||||
memberId: string;
|
||||
@@ -622,15 +845,22 @@ interface IBatchProcessEvent {
|
||||
firstOffset: string;
|
||||
lastOffset: string;
|
||||
}
|
||||
export type ConsumerStartBatchProcessEvent = InstrumentationEvent<
|
||||
IBatchProcessEvent
|
||||
>;
|
||||
export type ConsumerStartBatchProcessEvent = InstrumentationEvent<IBatchProcessEvent>;
|
||||
export type ConsumerEndBatchProcessEvent = InstrumentationEvent<
|
||||
IBatchProcessEvent & { duration: number }
|
||||
>;
|
||||
export type ConsumerCrashEvent = InstrumentationEvent<{
|
||||
error: Error;
|
||||
groupId: string;
|
||||
restart: boolean;
|
||||
}>;
|
||||
export type ConsumerReceivedUnsubcribedTopicsEvent = InstrumentationEvent<{
|
||||
groupId: string;
|
||||
generationId: number;
|
||||
memberId: string;
|
||||
assignedTopics: string[];
|
||||
topicsSubscribed: string[];
|
||||
topicsNotSubscribed: string[];
|
||||
}>;
|
||||
|
||||
export interface OffsetsByTopicPartition {
|
||||
@@ -648,7 +878,7 @@ export interface EachBatchPayload {
|
||||
resolveOffset(offset: string): void;
|
||||
heartbeat(): Promise<void>;
|
||||
commitOffsetsIfNecessary(offsets?: Offsets): Promise<void>;
|
||||
uncommittedOffsets(): Promise<OffsetsByTopicPartition>;
|
||||
uncommittedOffsets(): OffsetsByTopicPartition;
|
||||
isRunning(): boolean;
|
||||
isStale(): boolean;
|
||||
}
|
||||
@@ -665,25 +895,29 @@ export type ConsumerEachMessagePayload = EachMessagePayload;
|
||||
*/
|
||||
export type ConsumerEachBatchPayload = EachBatchPayload;
|
||||
|
||||
export interface Consumer {
|
||||
export type ConsumerRunConfig = {
|
||||
autoCommit?: boolean;
|
||||
autoCommitInterval?: number | null;
|
||||
autoCommitThreshold?: number | null;
|
||||
eachBatchAutoResolve?: boolean;
|
||||
partitionsConsumedConcurrently?: number;
|
||||
eachBatch?: (payload: EachBatchPayload) => Promise<void>;
|
||||
eachMessage?: (payload: EachMessagePayload) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ConsumerSubscribeTopic = {
|
||||
topic: string | RegExp;
|
||||
fromBeginning?: boolean;
|
||||
};
|
||||
|
||||
export type Consumer = {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
subscribe(topic: {
|
||||
topic: string | RegExp;
|
||||
fromBeginning?: boolean;
|
||||
}): Promise<void>;
|
||||
subscribe(topic: ConsumerSubscribeTopic): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
run(config?: {
|
||||
autoCommit?: boolean;
|
||||
autoCommitInterval?: number | null;
|
||||
autoCommitThreshold?: number | null;
|
||||
eachBatchAutoResolve?: boolean;
|
||||
partitionsConsumedConcurrently?: number;
|
||||
eachBatch?: (payload: EachBatchPayload) => Promise<void>;
|
||||
eachMessage?: (payload: EachMessagePayload) => Promise<void>;
|
||||
}): Promise<void>;
|
||||
run(config?: ConsumerRunConfig): Promise<void>;
|
||||
commitOffsets(
|
||||
topicPartitions: Array<TopicPartitionOffsetAndMedata>,
|
||||
topicPartitions: Array<TopicPartitionOffsetAndMetadata>,
|
||||
): Promise<void>;
|
||||
seek(topicPartition: {
|
||||
topic: string;
|
||||
@@ -692,14 +926,15 @@ export interface Consumer {
|
||||
}): void;
|
||||
describeGroup(): Promise<GroupDescription>;
|
||||
pause(topics: Array<{ topic: string; partitions?: number[] }>): void;
|
||||
paused(): TopicPartitions[];
|
||||
resume(topics: Array<{ topic: string; partitions?: number[] }>): void;
|
||||
on(
|
||||
eventName: ValueOf<ConsumerEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): void;
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
logger(): Logger;
|
||||
events: ConsumerEvents;
|
||||
}
|
||||
};
|
||||
|
||||
export enum CompressionTypes {
|
||||
None = 0,
|
||||
@@ -715,3 +950,186 @@ export let CompressionCodecs: {
|
||||
[CompressionTypes.LZ4]: () => any;
|
||||
[CompressionTypes.ZSTD]: () => any;
|
||||
};
|
||||
|
||||
export declare class KafkaJSError extends Error {
|
||||
readonly message: Error['message'];
|
||||
readonly name: string;
|
||||
readonly retriable: boolean;
|
||||
readonly helpUrl?: string;
|
||||
|
||||
constructor(e: Error | string, metadata?: KafkaJSErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSNonRetriableError extends KafkaJSError {
|
||||
constructor(e: Error | string);
|
||||
}
|
||||
|
||||
export declare class KafkaJSProtocolError extends KafkaJSError {
|
||||
readonly code: number;
|
||||
readonly type: string;
|
||||
constructor(e: Error | string);
|
||||
}
|
||||
|
||||
export declare class KafkaJSOffsetOutOfRange extends KafkaJSProtocolError {
|
||||
readonly topic: string;
|
||||
readonly partition: number;
|
||||
constructor(e: Error | string, metadata?: KafkaJSOffsetOutOfRangeMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSNumberOfRetriesExceeded extends KafkaJSNonRetriableError {
|
||||
readonly stack: string;
|
||||
readonly originalError: Error;
|
||||
readonly retryCount: number;
|
||||
readonly retryTime: number;
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSNumberOfRetriesExceededMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSConnectionError extends KafkaJSError {
|
||||
readonly broker: string;
|
||||
constructor(e: Error | string, metadata?: KafkaJSConnectionErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSRequestTimeoutError extends KafkaJSError {
|
||||
readonly broker: string;
|
||||
readonly correlationId: number;
|
||||
readonly createdAt: number;
|
||||
readonly sentAt: number;
|
||||
readonly pendingDuration: number;
|
||||
constructor(e: Error | string, metadata?: KafkaJSRequestTimeoutErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSMetadataNotLoaded extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSTopicMetadataNotLoaded extends KafkaJSMetadataNotLoaded {
|
||||
readonly topic: string;
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSTopicMetadataNotLoadedMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSStaleTopicMetadataAssignment extends KafkaJSError {
|
||||
readonly topic: string;
|
||||
readonly unknownPartitions: number;
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSStaleTopicMetadataAssignmentMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSServerDoesNotSupportApiKey extends KafkaJSNonRetriableError {
|
||||
readonly apiKey: number;
|
||||
readonly apiName: string;
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSServerDoesNotSupportApiKeyMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSBrokerNotFound extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSPartialMessageError extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSSASLAuthenticationError extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSGroupCoordinatorNotFound extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSNotImplemented extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSTimeout extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSLockTimeout extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSUnsupportedMagicByteInMessageSet extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSDeleteGroupsError extends KafkaJSError {
|
||||
readonly groups: DeleteGroupsResult[];
|
||||
constructor(e: Error | string, groups?: KafkaJSDeleteGroupsErrorGroups[]);
|
||||
}
|
||||
|
||||
export declare class KafkaJSDeleteTopicRecordsError extends KafkaJSError {
|
||||
constructor(metadata: KafkaJSDeleteTopicRecordsErrorTopic);
|
||||
}
|
||||
|
||||
export interface KafkaJSDeleteGroupsErrorGroups {
|
||||
groupId: string;
|
||||
errorCode: number;
|
||||
error: KafkaJSError;
|
||||
}
|
||||
|
||||
export interface KafkaJSDeleteTopicRecordsErrorTopic {
|
||||
topic: string;
|
||||
partitions: KafkaJSDeleteTopicRecordsErrorPartition[];
|
||||
}
|
||||
|
||||
export interface KafkaJSDeleteTopicRecordsErrorPartition {
|
||||
partition: number;
|
||||
offset: string;
|
||||
error: KafkaJSError;
|
||||
}
|
||||
|
||||
export interface KafkaJSErrorMetadata {
|
||||
retriable?: boolean;
|
||||
topic?: string;
|
||||
partitionId?: number;
|
||||
metadata?: PartitionMetadata;
|
||||
}
|
||||
|
||||
export interface KafkaJSOffsetOutOfRangeMetadata {
|
||||
topic: string;
|
||||
partition: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSNumberOfRetriesExceededMetadata {
|
||||
retryCount: number;
|
||||
retryTime: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSConnectionErrorMetadata {
|
||||
broker?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export interface KafkaJSRequestTimeoutErrorMetadata {
|
||||
broker: string;
|
||||
clientId: string;
|
||||
correlationId: number;
|
||||
createdAt: number;
|
||||
sentAt: number;
|
||||
pendingDuration: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSTopicMetadataNotLoadedMetadata {
|
||||
topic: string;
|
||||
}
|
||||
|
||||
export interface KafkaJSStaleTopicMetadataAssignmentMetadata {
|
||||
topic: string;
|
||||
unknownPartitions: PartitionMetadata[];
|
||||
}
|
||||
|
||||
export interface KafkaJSServerDoesNotSupportApiKeyMetadata {
|
||||
apiKey: number;
|
||||
apiName: string;
|
||||
}
|
||||
|
||||
@@ -9,3 +9,22 @@ export interface RmqUrl {
|
||||
heartbeat?: number;
|
||||
vhost?: string;
|
||||
}
|
||||
|
||||
export interface AmqpConnectionManagerSocketOptions {
|
||||
reconnectTimeInSeconds?: number;
|
||||
heartbeatIntervalInSeconds?: number;
|
||||
findServers?: () => string | string[];
|
||||
connectionOptions?: any;
|
||||
}
|
||||
|
||||
export interface AmqplibQueueOptions {
|
||||
durable?: boolean;
|
||||
autoDelete?: boolean;
|
||||
arguments?: any;
|
||||
messageTtl?: number;
|
||||
expires?: number;
|
||||
deadLetterExchange?: string;
|
||||
deadLetterRoutingKey?: string;
|
||||
maxLength?: number;
|
||||
maxPriority?: number;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './json-socket';
|
||||
export * from './kafka-logger';
|
||||
export * from './kafka-parser';
|
||||
export * from './kafka-round-robin-partition-assigner';
|
||||
export * from './kafka-reply-partition-assigner';
|
||||
|
||||
202
packages/microservices/helpers/kafka-reply-partition-assigner.ts
Normal file
202
packages/microservices/helpers/kafka-reply-partition-assigner.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { isUndefined } from '@nestjs/common/utils/shared.utils';
|
||||
import { ClientKafka } from '../client/client-kafka';
|
||||
import {
|
||||
Cluster,
|
||||
GroupMember,
|
||||
GroupMemberAssignment,
|
||||
GroupState,
|
||||
MemberMetadata,
|
||||
} from '../external/kafka.interface';
|
||||
|
||||
let kafkaPackage: any = {};
|
||||
|
||||
export class KafkaReplyPartitionAssigner {
|
||||
readonly name = 'NestReplyPartitionAssigner';
|
||||
readonly version = 1;
|
||||
|
||||
constructor(
|
||||
private readonly clientKafka: ClientKafka,
|
||||
private readonly config: {
|
||||
cluster: Cluster;
|
||||
},
|
||||
) {
|
||||
kafkaPackage = loadPackage(
|
||||
'kafkajs',
|
||||
KafkaReplyPartitionAssigner.name,
|
||||
() => require('kafkajs'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This process can result in imbalanced assignments
|
||||
* @param {array} members array of members, e.g: [{ memberId: 'test-5f93f5a3' }]
|
||||
* @param {array} topics
|
||||
* @param {Buffer} userData
|
||||
* @returns {array} object partitions per topic per member
|
||||
*/
|
||||
public async assign(group: {
|
||||
members: GroupMember[];
|
||||
topics: string[];
|
||||
}): Promise<GroupMemberAssignment[]> {
|
||||
const assignment = {};
|
||||
const previousAssignment = {};
|
||||
|
||||
const membersCount = group.members.length;
|
||||
const decodedMembers = group.members.map(member =>
|
||||
this.decodeMember(member),
|
||||
);
|
||||
const sortedMemberIds = decodedMembers
|
||||
.map(member => member.memberId)
|
||||
.sort();
|
||||
|
||||
// build the previous assignment and an inverse map of topic > partition > memberId for lookup
|
||||
decodedMembers.forEach(member => {
|
||||
if (
|
||||
!previousAssignment[member.memberId] &&
|
||||
Object.keys(member.previousAssignment).length > 0
|
||||
) {
|
||||
previousAssignment[member.memberId] = member.previousAssignment;
|
||||
}
|
||||
});
|
||||
|
||||
// build a collection of topics and partitions
|
||||
const topicsPartitions = group.topics
|
||||
.map(topic => {
|
||||
const partitionMetadata = this.config.cluster.findTopicPartitionMetadata(
|
||||
topic,
|
||||
);
|
||||
return partitionMetadata.map(m => {
|
||||
return {
|
||||
topic,
|
||||
partitionId: m.partitionId,
|
||||
};
|
||||
});
|
||||
})
|
||||
.reduce((acc, val) => acc.concat(val), []);
|
||||
|
||||
// create the new assignment by populating the members with the first partition of the topics
|
||||
sortedMemberIds.forEach(assignee => {
|
||||
if (!assignment[assignee]) {
|
||||
assignment[assignee] = {};
|
||||
}
|
||||
|
||||
// add topics to each member
|
||||
group.topics.forEach(topic => {
|
||||
if (!assignment[assignee][topic]) {
|
||||
assignment[assignee][topic] = [];
|
||||
}
|
||||
|
||||
// see if the topic and partition belong to a previous assignment
|
||||
if (
|
||||
previousAssignment[assignee] &&
|
||||
!isUndefined(previousAssignment[assignee][topic])
|
||||
) {
|
||||
// take the minimum partition since replies will be sent to the minimum partition
|
||||
const firstPartition = previousAssignment[assignee][topic];
|
||||
|
||||
// create the assignment with the first partition
|
||||
assignment[assignee][topic].push(firstPartition);
|
||||
|
||||
// find and remove this topic and partition from the topicPartitions to be assigned later
|
||||
const topicsPartitionsIndex = topicsPartitions.findIndex(
|
||||
topicPartition => {
|
||||
return (
|
||||
topicPartition.topic === topic &&
|
||||
topicPartition.partitionId === firstPartition
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// only continue if we found a partition matching this topic
|
||||
if (topicsPartitionsIndex !== -1) {
|
||||
// remove inline
|
||||
topicsPartitions.splice(topicsPartitionsIndex, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// check for member topics that have a partition length of 0
|
||||
sortedMemberIds.forEach(assignee => {
|
||||
group.topics.forEach(topic => {
|
||||
// only continue if there are no partitions for assignee's topic
|
||||
if (assignment[assignee][topic].length === 0) {
|
||||
// find the first partition for this topic
|
||||
const topicsPartitionsIndex = topicsPartitions.findIndex(
|
||||
topicPartition => {
|
||||
return topicPartition.topic === topic;
|
||||
},
|
||||
);
|
||||
|
||||
if (topicsPartitionsIndex !== -1) {
|
||||
// find and set the topic partition
|
||||
const partition =
|
||||
topicsPartitions[topicsPartitionsIndex].partitionId;
|
||||
|
||||
assignment[assignee][topic].push(partition);
|
||||
|
||||
// remove this partition from the topics partitions collection
|
||||
topicsPartitions.splice(topicsPartitionsIndex, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// then balance out the rest of the topic partitions across the members
|
||||
const insertAssignmentsByTopic = (topicPartition, i) => {
|
||||
const assignee = sortedMemberIds[i % membersCount];
|
||||
|
||||
assignment[assignee][topicPartition.topic].push(
|
||||
topicPartition.partitionId,
|
||||
);
|
||||
};
|
||||
|
||||
// build the assignments
|
||||
topicsPartitions.forEach(insertAssignmentsByTopic);
|
||||
|
||||
// encode the end result
|
||||
return Object.keys(assignment).map(memberId => ({
|
||||
memberId,
|
||||
memberAssignment: kafkaPackage.AssignerProtocol.MemberAssignment.encode({
|
||||
version: this.version,
|
||||
assignment: assignment[memberId],
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
public protocol(subscription: {
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
}): GroupState {
|
||||
const stringifiedUserData = JSON.stringify({
|
||||
previousAssignment: this.getPreviousAssignment(),
|
||||
});
|
||||
subscription.userData = Buffer.from(stringifiedUserData);
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
metadata: kafkaPackage.AssignerProtocol.MemberMetadata.encode({
|
||||
version: this.version,
|
||||
topics: subscription.topics,
|
||||
userData: subscription.userData,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
public getPreviousAssignment() {
|
||||
return this.clientKafka.getConsumerAssignments();
|
||||
}
|
||||
|
||||
public decodeMember(member: GroupMember) {
|
||||
const memberMetadata = kafkaPackage.AssignerProtocol.MemberMetadata.decode(
|
||||
member.memberMetadata,
|
||||
) as MemberMetadata;
|
||||
const memberUserData = JSON.parse(memberMetadata.userData.toString());
|
||||
|
||||
return {
|
||||
memberId: member.memberId,
|
||||
previousAssignment: memberUserData.previousAssignment,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import {
|
||||
Cluster,
|
||||
GroupMember,
|
||||
GroupMemberAssignment,
|
||||
GroupState,
|
||||
MemberMetadata,
|
||||
} from '../external/kafka.interface';
|
||||
|
||||
let kafkaPackage: any = {};
|
||||
|
||||
const time = process.hrtime();
|
||||
|
||||
export class KafkaRoundRobinPartitionAssigner {
|
||||
readonly name = 'RoundRobinByTime';
|
||||
readonly version = 1;
|
||||
|
||||
constructor(private readonly config: { cluster: Cluster }) {
|
||||
kafkaPackage = loadPackage(
|
||||
'kafkajs',
|
||||
KafkaRoundRobinPartitionAssigner.name,
|
||||
() => require('kafkajs'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This process can result in imbalanced assignments
|
||||
* @param {array} members array of members, e.g: [{ memberId: 'test-5f93f5a3' }]
|
||||
* @param {array} topics
|
||||
* @param {Buffer} userData
|
||||
* @returns {array} object partitions per topic per member
|
||||
*/
|
||||
public async assign(group: {
|
||||
members: GroupMember[];
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
}): Promise<GroupMemberAssignment[]> {
|
||||
const membersCount = group.members.length;
|
||||
const assignment = {};
|
||||
|
||||
const sortedMembers = group.members
|
||||
.map(member => this.mapToTimeAndMemberId(member))
|
||||
.sort((a, b) => this.sortByTime(a, b))
|
||||
.map(member => member.memberId);
|
||||
|
||||
sortedMembers.forEach(memberId => {
|
||||
assignment[memberId] = {};
|
||||
});
|
||||
|
||||
const insertAssignmentsByTopic = (topic: string) => {
|
||||
const partitionMetadata = this.config.cluster.findTopicPartitionMetadata(
|
||||
topic,
|
||||
);
|
||||
const partitions = partitionMetadata.map(m => m.partitionId);
|
||||
sortedMembers.forEach((memberId, i) => {
|
||||
if (!assignment[memberId][topic]) {
|
||||
assignment[memberId][topic] = [];
|
||||
}
|
||||
|
||||
assignment[memberId][topic].push(
|
||||
...partitions.filter(id => id % membersCount === i),
|
||||
);
|
||||
});
|
||||
};
|
||||
group.topics.forEach(insertAssignmentsByTopic);
|
||||
|
||||
return Object.keys(assignment).map(memberId => ({
|
||||
memberId,
|
||||
memberAssignment: kafkaPackage.AssignerProtocol.MemberAssignment.encode({
|
||||
version: this.version,
|
||||
assignment: assignment[memberId],
|
||||
userData: group.userData,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
public protocol(subscription: {
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
}): GroupState {
|
||||
const stringifiedTimeObject = JSON.stringify({
|
||||
time: this.getTime(),
|
||||
});
|
||||
subscription.userData = Buffer.from(stringifiedTimeObject);
|
||||
return {
|
||||
name: this.name,
|
||||
metadata: kafkaPackage.AssignerProtocol.MemberMetadata.encode({
|
||||
version: this.version,
|
||||
topics: subscription.topics,
|
||||
userData: subscription.userData,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
public getTime(): [number, number] {
|
||||
return time;
|
||||
}
|
||||
|
||||
public mapToTimeAndMemberId(member: GroupMember) {
|
||||
const memberMetadata = kafkaPackage.AssignerProtocol.MemberMetadata.decode(
|
||||
member.memberMetadata,
|
||||
) as MemberMetadata;
|
||||
const memberUserData = JSON.parse(memberMetadata.userData.toString());
|
||||
|
||||
return {
|
||||
memberId: member.memberId,
|
||||
time: memberUserData.time,
|
||||
};
|
||||
}
|
||||
|
||||
public sortByTime(a: Record<'time', number[]>, b: Record<'time', number[]>) {
|
||||
// if seconds are equal sort by nanoseconds
|
||||
if (a.time[0] === b.time[0]) {
|
||||
return a.time[1] - b.time[1];
|
||||
}
|
||||
// sort by seconds
|
||||
return a.time[0] - b.time[0];
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Type } from '@nestjs/common';
|
||||
import { ClientProxy } from '../client';
|
||||
import { Transport } from '../enums/transport.enum';
|
||||
import { Deserializer } from './deserializer.interface';
|
||||
import {
|
||||
@@ -19,6 +21,11 @@ export type ClientOptions =
|
||||
| TcpClientOptions
|
||||
| RmqOptions;
|
||||
|
||||
export interface CustomClientOptions {
|
||||
customClass: Type<ClientProxy>;
|
||||
options?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface TcpClientOptions {
|
||||
transport: Transport.TCP;
|
||||
options?: {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Transport } from '../enums/transport.enum';
|
||||
import { ChannelOptions } from '../external/grpc-options.interface';
|
||||
import {
|
||||
CompressionTypes,
|
||||
ConsumerConfig,
|
||||
ConsumerRunConfig,
|
||||
ConsumerSubscribeTopic,
|
||||
KafkaConfig,
|
||||
ProducerConfig,
|
||||
} from '../external/kafka-options.interface';
|
||||
ProducerRecord,
|
||||
} from '../external/kafka.interface';
|
||||
import { MqttClientOptions } from '../external/mqtt-options.interface';
|
||||
import { ClientOpts } from '../external/redis.interface';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
import { Server } from '../server/server';
|
||||
import { CustomTransportStrategy } from './custom-transport-strategy.interface';
|
||||
import { Deserializer } from './deserializer.interface';
|
||||
import { Serializer } from './serializer.interface';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
|
||||
export type MicroserviceOptions =
|
||||
| GrpcOptions
|
||||
@@ -25,7 +27,7 @@ export type MicroserviceOptions =
|
||||
| CustomStrategy;
|
||||
|
||||
export interface CustomStrategy {
|
||||
strategy: Server & CustomTransportStrategy;
|
||||
strategy: CustomTransportStrategy;
|
||||
options?: {};
|
||||
}
|
||||
|
||||
@@ -101,13 +103,19 @@ export interface MqttOptions {
|
||||
export interface NatsOptions {
|
||||
transport?: Transport.NATS;
|
||||
options?: {
|
||||
encoding?: string;
|
||||
url?: string;
|
||||
name?: string;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
maxPingOut?: number;
|
||||
maxReconnectAttempts?: number;
|
||||
reconnectTimeWait?: number;
|
||||
reconnectJitter?: number;
|
||||
reconnectJitterTLS?: number;
|
||||
reconnectDelayHandler?: any;
|
||||
servers?: string[];
|
||||
nkey?: any;
|
||||
reconnect?: boolean;
|
||||
pedantic?: boolean;
|
||||
tls?: any;
|
||||
@@ -117,6 +125,18 @@ export interface NatsOptions {
|
||||
userJWT?: string;
|
||||
nonceSigner?: any;
|
||||
userCreds?: any;
|
||||
useOldRequestStyle?: boolean;
|
||||
pingInterval?: number;
|
||||
preserveBuffers?: boolean;
|
||||
waitOnFirstConnect?: boolean;
|
||||
verbose?: boolean;
|
||||
noEcho?: boolean;
|
||||
noRandomize?: boolean;
|
||||
timeout?: number;
|
||||
token?: string;
|
||||
yieldTime?: number;
|
||||
tokenHandler?: any;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,8 +147,8 @@ export interface RmqOptions {
|
||||
queue?: string;
|
||||
prefetchCount?: number;
|
||||
isGlobalPrefetchCount?: boolean;
|
||||
queueOptions?: any;
|
||||
socketOptions?: any;
|
||||
queueOptions?: any; // AmqplibQueueOptions;
|
||||
socketOptions?: any; // AmqpConnectionManagerSocketOptions;
|
||||
noAck?: boolean;
|
||||
serializer?: Serializer;
|
||||
deserializer?: Deserializer;
|
||||
@@ -140,24 +160,13 @@ export interface RmqOptions {
|
||||
export interface KafkaOptions {
|
||||
transport?: Transport.KAFKA;
|
||||
options?: {
|
||||
postfixId?: string;
|
||||
client?: KafkaConfig;
|
||||
consumer?: ConsumerConfig;
|
||||
run?: {
|
||||
autoCommit?: boolean;
|
||||
autoCommitInterval?: number | null;
|
||||
autoCommitThreshold?: number | null;
|
||||
eachBatchAutoResolve?: boolean;
|
||||
partitionsConsumedConcurrently?: number;
|
||||
};
|
||||
subscribe?: {
|
||||
fromBeginning?: boolean;
|
||||
};
|
||||
run?: Omit<ConsumerRunConfig, 'eachBatch' | 'eachMessage'>;
|
||||
subscribe?: Omit<ConsumerSubscribeTopic, 'topic'>;
|
||||
producer?: ProducerConfig;
|
||||
send?: {
|
||||
acks?: number;
|
||||
timeout?: number;
|
||||
compression?: CompressionTypes;
|
||||
};
|
||||
send?: Omit<ProducerRecord, 'topic' | 'messages'>;
|
||||
serializer?: Serializer;
|
||||
deserializer?: Deserializer;
|
||||
};
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { ClientOptions } from '../../interfaces';
|
||||
import { ClientOptions, CustomClientOptions } from '../../interfaces';
|
||||
import { Type, Provider, ModuleMetadata } from '@nestjs/common/interfaces';
|
||||
|
||||
export type ClientProviderOptions = ClientOptions & {
|
||||
export type ClientProvider = ClientOptions | CustomClientOptions;
|
||||
|
||||
export type ClientProviderOptions = ClientProvider & {
|
||||
name: string | symbol;
|
||||
};
|
||||
|
||||
export type ClientsModuleOptions = Array<ClientProviderOptions>;
|
||||
|
||||
export interface ClientsModuleOptionsFactory {
|
||||
createClientOptions(): Promise<ClientOptions> | ClientOptions;
|
||||
createClientOptions(): Promise<ClientProvider> | ClientProvider;
|
||||
}
|
||||
|
||||
export interface ClientsProviderAsyncOptions
|
||||
extends Pick<ModuleMetadata, 'imports'> {
|
||||
useExisting?: Type<ClientsModuleOptionsFactory>;
|
||||
useClass?: Type<ClientsModuleOptionsFactory>;
|
||||
useFactory?: (...args: any[]) => Promise<ClientOptions> | ClientOptions;
|
||||
useFactory?: (...args: any[]) => Promise<ClientProvider> | ClientProvider;
|
||||
inject?: any[];
|
||||
extraProviders?: Provider[];
|
||||
name: string | symbol;
|
||||
|
||||
@@ -123,7 +123,7 @@ export class NestMicroservice
|
||||
!this.isInitialized && (await this.registerModules());
|
||||
|
||||
this.logger.log(MESSAGES.MICROSERVICE_READY);
|
||||
return new Promise(resolve => this.server.listen(resolve));
|
||||
return new Promise<void>(resolve => this.server.listen(resolve));
|
||||
}
|
||||
|
||||
public async close(): Promise<any> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/microservices",
|
||||
"version": "7.5.5",
|
||||
"version": "7.6.10",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@microservices)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -19,15 +19,54 @@
|
||||
"dependencies": {
|
||||
"iterare": "1.2.1",
|
||||
"json-socket": "0.3.0",
|
||||
"tslib": "2.0.3"
|
||||
"tslib": "2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "7.5.5",
|
||||
"@nestjs/core": "7.5.5"
|
||||
"@nestjs/common": "7.6.10",
|
||||
"@nestjs/core": "7.6.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.0.0",
|
||||
"@nestjs/core": "^7.0.0",
|
||||
"@nestjs/websockets": "^7.0.0",
|
||||
"amqp-connection-manager": "*",
|
||||
"amqplib": "*",
|
||||
"cache-manager": "*",
|
||||
"grpc": "*",
|
||||
"kafkajs": "*",
|
||||
"mqtt": "*",
|
||||
"nats": "*",
|
||||
"redis": "*",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/websockets": {
|
||||
"optional": true
|
||||
},
|
||||
"cache-manager": {
|
||||
"optional": true
|
||||
},
|
||||
"grpc": {
|
||||
"optional": true
|
||||
},
|
||||
"kafkajs": {
|
||||
"optional": true
|
||||
},
|
||||
"mqtt": {
|
||||
"optional": true
|
||||
},
|
||||
"nats": {
|
||||
"optional": true
|
||||
},
|
||||
"redis": {
|
||||
"optional": true
|
||||
},
|
||||
"amqplib": {
|
||||
"optional": true
|
||||
},
|
||||
"amqp-connection-manager": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
KafkaMessage,
|
||||
Message,
|
||||
Producer,
|
||||
RecordMetadata,
|
||||
} from '../external/kafka.interface';
|
||||
import { KafkaLogger, KafkaParser } from '../helpers';
|
||||
import {
|
||||
@@ -50,17 +51,16 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
this.getOptionsProp(this.options, 'client') || ({} as KafkaConfig);
|
||||
const consumerOptions =
|
||||
this.getOptionsProp(this.options, 'consumer') || ({} as ConsumerConfig);
|
||||
const postfixId =
|
||||
this.getOptionsProp(this.options, 'postfixId') || '-server';
|
||||
|
||||
this.brokers = clientOptions.brokers || [KAFKA_DEFAULT_BROKER];
|
||||
|
||||
// append a unique id to the clientId and groupId
|
||||
// so they don't collide with a microservices client
|
||||
this.clientId =
|
||||
(clientOptions.clientId || KAFKA_DEFAULT_CLIENT) +
|
||||
(clientOptions.clientIdPostfix || '-server');
|
||||
this.groupId =
|
||||
(consumerOptions.groupId || KAFKA_DEFAULT_GROUP) +
|
||||
(clientOptions.clientIdPostfix || '-server');
|
||||
(clientOptions.clientId || KAFKA_DEFAULT_CLIENT) + postfixId;
|
||||
this.groupId = (consumerOptions.groupId || KAFKA_DEFAULT_GROUP) + postfixId;
|
||||
|
||||
kafkaPackage = this.loadPackage('kafkajs', ServerKafka.name, () =>
|
||||
require('kafkajs'),
|
||||
@@ -75,9 +75,9 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
await this.start(callback);
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.consumer && this.consumer.disconnect();
|
||||
this.producer && this.producer.disconnect();
|
||||
public async close(): Promise<void> {
|
||||
this.consumer && (await this.consumer.disconnect());
|
||||
this.producer && (await this.producer.disconnect());
|
||||
this.consumer = null;
|
||||
this.producer = null;
|
||||
this.client = null;
|
||||
@@ -130,7 +130,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
replyTopic: string,
|
||||
replyPartition: string,
|
||||
correlationId: string,
|
||||
): (data: any) => any {
|
||||
): (data: any) => Promise<RecordMetadata[]> {
|
||||
return (data: any) =>
|
||||
this.sendMessage(data, replyTopic, replyPartition, correlationId);
|
||||
}
|
||||
@@ -184,7 +184,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
replyTopic: string,
|
||||
replyPartition: string,
|
||||
correlationId: string,
|
||||
): void {
|
||||
): Promise<RecordMetadata[]> {
|
||||
const outgoingMessage = this.serializer.serialize(message.response);
|
||||
this.assignReplyPartition(replyPartition, outgoingMessage);
|
||||
this.assignCorrelationIdHeader(correlationId, outgoingMessage);
|
||||
@@ -198,7 +198,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
},
|
||||
this.options.send || {},
|
||||
);
|
||||
this.producer.send(replyMessage);
|
||||
return this.producer.send(replyMessage);
|
||||
}
|
||||
|
||||
public assignIsDisposedHeader(
|
||||
|
||||
@@ -18,13 +18,13 @@ import {
|
||||
} from '../constants';
|
||||
import { RmqContext } from '../ctx-host';
|
||||
import { Transport } from '../enums';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
import { CustomTransportStrategy, RmqOptions } from '../interfaces';
|
||||
import {
|
||||
IncomingRequest,
|
||||
OutgoingResponse,
|
||||
} from '../interfaces/packet.interface';
|
||||
import { Server } from './server';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
|
||||
let rqmPackage: any = {};
|
||||
|
||||
|
||||
@@ -61,14 +61,16 @@ export abstract class Server {
|
||||
|
||||
public send(
|
||||
stream$: Observable<any>,
|
||||
respond: (data: WritePacket) => void,
|
||||
respond: (data: WritePacket) => unknown | Promise<unknown>,
|
||||
): Subscription {
|
||||
let dataBuffer: WritePacket[] = null;
|
||||
const scheduleOnNextTick = (data: WritePacket) => {
|
||||
if (!dataBuffer) {
|
||||
dataBuffer = [data];
|
||||
process.nextTick(() => {
|
||||
dataBuffer.forEach(buffer => respond(buffer));
|
||||
process.nextTick(async () => {
|
||||
for (const item of dataBuffer) {
|
||||
await respond(item);
|
||||
}
|
||||
dataBuffer = null;
|
||||
});
|
||||
} else if (!data.isDisposed) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as sinon from 'sinon';
|
||||
import { ClientKafka } from '../../client/client-kafka';
|
||||
import { NO_MESSAGE_HANDLER } from '../../constants';
|
||||
import { KafkaHeaders } from '../../enums';
|
||||
import { InvalidKafkaClientTopicPartitionException } from '../../errors/invalid-kafka-client-topic-partition.exception';
|
||||
import { InvalidKafkaClientTopicException } from '../../errors/invalid-kafka-client-topic.exception';
|
||||
import {
|
||||
ConsumerGroupJoinEvent,
|
||||
@@ -269,6 +268,7 @@ describe('ClientKafka', () => {
|
||||
|
||||
expect(createClientStub.calledOnce).to.be.true;
|
||||
expect(producerStub.calledOnce).to.be.true;
|
||||
|
||||
expect(consumerStub.calledOnce).to.be.true;
|
||||
|
||||
expect(on.calledOnce).to.be.true;
|
||||
@@ -314,13 +314,19 @@ describe('ClientKafka', () => {
|
||||
memberId: 'member-1',
|
||||
memberAssignment: {
|
||||
'topic-a': [0, 1, 2],
|
||||
'topic-b': [3, 4, 5],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
client['setConsumerAssignments'](consumerAssignments);
|
||||
|
||||
expect(client['consumerAssignments']).to.deep.eq(
|
||||
consumerAssignments.payload.memberAssignment,
|
||||
// consumerAssignments.payload.memberAssignment,
|
||||
{
|
||||
'topic-a': 0,
|
||||
'topic-b': 3,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -493,10 +499,22 @@ describe('ClientKafka', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConsumerAssignments', () => {
|
||||
it('should get consumer assignments', () => {
|
||||
client['consumerAssignments'] = {
|
||||
[replyTopic]: 0,
|
||||
};
|
||||
|
||||
const result = client.getConsumerAssignments();
|
||||
|
||||
expect(result).to.deep.eq(client['consumerAssignments']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReplyTopicPartition', () => {
|
||||
it('should get reply partition', () => {
|
||||
client['consumerAssignments'] = {
|
||||
[replyTopic]: [0],
|
||||
[replyTopic]: 0,
|
||||
};
|
||||
|
||||
const result = client['getReplyTopicPartition'](replyTopic);
|
||||
@@ -504,19 +522,17 @@ describe('ClientKafka', () => {
|
||||
expect(result).to.eq('0');
|
||||
});
|
||||
|
||||
it('should throw error when the topic is being consumed but is not assigned partitions', () => {
|
||||
client['consumerAssignments'] = {
|
||||
[replyTopic]: [],
|
||||
};
|
||||
it('should throw error when the topic is not being consumed', () => {
|
||||
client['consumerAssignments'] = {};
|
||||
|
||||
expect(() => client['getReplyTopicPartition'](replyTopic)).to.throw(
|
||||
InvalidKafkaClientTopicPartitionException,
|
||||
InvalidKafkaClientTopicException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when the topic is not being consumer', () => {
|
||||
it('should throw error when the topic is not being consumed', () => {
|
||||
client['consumerAssignments'] = {
|
||||
[topic]: [],
|
||||
[topic]: undefined,
|
||||
};
|
||||
|
||||
expect(() => client['getReplyTopicPartition'](replyTopic)).to.throw(
|
||||
@@ -551,7 +567,7 @@ describe('ClientKafka', () => {
|
||||
'getReplyTopicPartition',
|
||||
);
|
||||
routingMapSetSpy = sinon.spy((client as any).routingMap, 'set');
|
||||
sendSpy = sinon.spy();
|
||||
sendSpy = sinon.spy(() => Promise.resolve());
|
||||
|
||||
// stub
|
||||
assignPacketIdStub = sinon
|
||||
@@ -568,7 +584,7 @@ describe('ClientKafka', () => {
|
||||
|
||||
// set
|
||||
client['consumerAssignments'] = {
|
||||
[replyTopic]: [parseFloat(replyPartition)],
|
||||
[replyTopic]: parseFloat(replyPartition),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import * as Kafka from 'kafkajs';
|
||||
import { KafkaReplyPartitionAssigner } from '../../helpers/kafka-reply-partition-assigner';
|
||||
import { ClientKafka } from '../../client/client-kafka';
|
||||
|
||||
describe('kafka reply partition assigner', () => {
|
||||
let cluster, topics, metadata, assigner, client;
|
||||
|
||||
let getConsumerAssignments: sinon.SinonSpy;
|
||||
let getPreviousAssignment: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
metadata = {};
|
||||
cluster = { findTopicPartitionMetadata: topic => metadata[topic] };
|
||||
client = new ClientKafka({});
|
||||
assigner = new KafkaReplyPartitionAssigner(client, { cluster });
|
||||
topics = ['topic-A', 'topic-B'];
|
||||
|
||||
getConsumerAssignments = sinon.spy(client, 'getConsumerAssignments');
|
||||
getPreviousAssignment = sinon.spy(assigner, 'getPreviousAssignment');
|
||||
|
||||
// reset previous assignments
|
||||
(client as any).consumerAssignments = {};
|
||||
});
|
||||
|
||||
describe('assign', () => {
|
||||
it('assign all partitions evenly', async () => {
|
||||
metadata['topic-A'] = Array(14)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
metadata['topic-B'] = Array(5)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
const members = [
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const assignment = await assigner.assign({ members, topics });
|
||||
|
||||
expect(assignment).to.deep.equal([
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [0, 4, 8, 12],
|
||||
'topic-B': [0],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [1, 5, 9, 13],
|
||||
'topic-B': [1],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [2, 6, 10],
|
||||
'topic-B': [2, 4],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [3, 7, 11],
|
||||
'topic-B': [3],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('re-assign', () => {
|
||||
it('assign all partitions evenly', async () => {
|
||||
metadata['topic-A'] = Array(11)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
metadata['topic-B'] = Array(7)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
const members = [
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {
|
||||
'topic-A': 0,
|
||||
'topic-B': 0,
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {
|
||||
'topic-A': 1,
|
||||
'topic-B': 1,
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {
|
||||
'topic-A': 2,
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const assignment = await assigner.assign({ members, topics });
|
||||
|
||||
expect(assignment).to.deep.equal([
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [1, 4, 8],
|
||||
'topic-B': [1, 5],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [3, 5, 9],
|
||||
'topic-B': [2, 6],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [0, 6, 10],
|
||||
'topic-B': [0],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [2, 7],
|
||||
'topic-B': [3, 4],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('protocol', () => {
|
||||
it('returns the assigner name and metadata', () => {
|
||||
// set previous assignments
|
||||
(client as any).consumerAssignments = {
|
||||
'topic-A': 0,
|
||||
'topic-B': 1,
|
||||
};
|
||||
|
||||
const protocol = assigner.protocol({ topics });
|
||||
|
||||
expect(getPreviousAssignment.calledOnce).to.be.true;
|
||||
expect(getConsumerAssignments.calledOnce).to.be.true;
|
||||
|
||||
expect(protocol).to.deep.equal({
|
||||
name: assigner.name,
|
||||
metadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics,
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: (client as any).consumerAssignments,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import { expect } from 'chai';
|
||||
import * as Kafka from 'kafkajs';
|
||||
import { KafkaRoundRobinPartitionAssigner } from '../../helpers/kafka-round-robin-partition-assigner';
|
||||
|
||||
describe('kafka round robin by time', () => {
|
||||
let cluster, topics, metadata, assigner;
|
||||
|
||||
beforeEach(() => {
|
||||
metadata = {};
|
||||
cluster = { findTopicPartitionMetadata: topic => metadata[topic] };
|
||||
assigner = new KafkaRoundRobinPartitionAssigner({ cluster });
|
||||
topics = ['topic-A', 'topic-B'];
|
||||
});
|
||||
|
||||
describe('assign', () => {
|
||||
it('assign all partitions evenly', async () => {
|
||||
metadata['topic-A'] = Array(14)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
metadata['topic-B'] = Array(5)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
const members = [
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
time: [0, 0], // process.hrtime()
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
time: [0, 1], // process.hrtime()
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
time: [1, 1], // process.hrtime()
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
time: [2, 0], // process.hrtime()
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const assignment = await assigner.assign({ members, topics });
|
||||
|
||||
expect(assignment).to.deep.equal([
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [0, 4, 8, 12],
|
||||
'topic-B': [0, 4],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [1, 5, 9, 13],
|
||||
'topic-B': [1],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [2, 6, 10],
|
||||
'topic-B': [2],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [3, 7, 11],
|
||||
'topic-B': [3],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('protocol', () => {
|
||||
it('returns the assigner name and metadata', () => {
|
||||
expect(assigner.protocol({ topics })).to.deep.equal({
|
||||
name: assigner.name,
|
||||
metadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics,
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
time: assigner.getTime(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,7 @@ describe('JsonSocket connection', () => {
|
||||
new Promise(callback => {
|
||||
clientSocket.sendMessage({ type: 'ping' }, callback);
|
||||
}),
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
clientSocket.on(MESSAGE_EVENT, (message: string) => {
|
||||
expect(message).to.deep.equal({ type: 'pong' });
|
||||
callback();
|
||||
@@ -53,16 +53,16 @@ describe('JsonSocket connection', () => {
|
||||
expect(clientSocket['isClosed']).to.equal(false);
|
||||
expect(serverSocket['isClosed']).to.equal(false);
|
||||
Promise.all([
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
clientSocket.sendMessage(longPayload, callback);
|
||||
}),
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
clientSocket.on(MESSAGE_EVENT, (message: { type: 'pong' }) => {
|
||||
expect(message).to.deep.equal({ type: 'pong' });
|
||||
callback();
|
||||
});
|
||||
}),
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
serverSocket.on(MESSAGE_EVENT, (message: { type: 'pong' }) => {
|
||||
expect(message).to.deep.equal(longPayload);
|
||||
serverSocket.sendMessage({ type: 'pong' }, callback);
|
||||
@@ -85,7 +85,7 @@ describe('JsonSocket connection', () => {
|
||||
return done(err);
|
||||
}
|
||||
Promise.all([
|
||||
new Promise(callback =>
|
||||
new Promise<void>(callback =>
|
||||
Promise.all(
|
||||
helpers
|
||||
.range(1, 100)
|
||||
@@ -97,7 +97,7 @@ describe('JsonSocket connection', () => {
|
||||
),
|
||||
).then(_ => callback()),
|
||||
),
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
let lastNumber = 0;
|
||||
serverSocket.on(MESSAGE_EVENT, (message: { number: number }) => {
|
||||
expect(message.number).to.deep.equal(lastNumber + 1);
|
||||
@@ -128,7 +128,7 @@ describe('JsonSocket connection', () => {
|
||||
})
|
||||
.then(
|
||||
() =>
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
expect(clientSocket['isClosed']).to.equal(true);
|
||||
expect(serverSocket['isClosed']).to.equal(true);
|
||||
callback();
|
||||
@@ -154,7 +154,7 @@ describe('JsonSocket connection', () => {
|
||||
})
|
||||
.then(
|
||||
() =>
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
expect(clientSocket['isClosed']).to.equal(true);
|
||||
expect(serverSocket['isClosed']).to.equal(true);
|
||||
callback();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user