Compare commits

...

65 Commits

Author SHA1 Message Date
Kamil Myśliwiec
4fac9c9bf2 chore(): remove http folder 2022-05-18 09:24:36 +02:00
Tony133
dd58e42501 chore(): remove http module deprecated 2022-05-17 17:53:30 +02:00
Kamil Mysliwiec
2362ade438 Merge pull request #9311 from woobottle/woo_bottle/change-reflectMetatData-params-order
Refactoring(core): Change ReflectMetadata Params Order
2022-05-17 17:04:26 +02:00
Kamil Myśliwiec
f11181071e chore(): resolve conflicts 2022-05-17 14:11:27 +02:00
Kamil Mysliwiec
11ac94e23e Merge pull request #8798 from nestjs/feat/migrate-to-ioredis
feat(microservices): migrate redis transporter to internally use ioredis package
2022-05-17 14:10:38 +02:00
Kamil Mysliwiec
cd62e31bcf Merge pull request #9522 from nestjs/dependabot/npm_and_yarn/mocha-10.0.0
chore(deps-dev): bump mocha from 9.2.2 to 10.0.0
2022-05-17 14:09:04 +02:00
Kamil Myśliwiec
34143d9e79 test(microservices): update outdated listeners controller test 2022-05-17 14:02:12 +02:00
Kamil Myśliwiec
cf42ed5b8d ci(): remove test_node_10 job 2022-05-17 13:39:29 +02:00
Kamil Myśliwiec
72c046dec7 fix(microservices): update ioredis options 2022-05-17 13:39:18 +02:00
Kamil Myśliwiec
1e8966b603 test(microservices): update server & client redis unit tests 2022-05-17 13:33:21 +02:00
Kamil Myśliwiec
40e2755c82 feat(microservices): migrate redis transporter to internally use ioredis package 2022-05-17 13:33:21 +02:00
dependabot[bot]
64e58b750c chore(deps-dev): bump mocha from 9.2.2 to 10.0.0
Bumps [mocha](https://github.com/mochajs/mocha) from 9.2.2 to 10.0.0.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v9.2.2...v10.0.0)

---
updated-dependencies:
- dependency-name: mocha
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-17 11:25:46 +00:00
Kamil Myśliwiec
61bcb7fe39 Merge branch 'alexfriesen-master' into 9.0.0 2022-05-17 13:24:07 +02:00
Kamil Myśliwiec
ddd95b810f chore(): resolve conflicts 2022-05-17 13:23:51 +02:00
Kamil Mysliwiec
707a3e069f Merge pull request #9587 from nestjs/fix/revert-interceptors-grpc
fix(microservices): revert grpc client interceptors (grpc-specific)
2022-05-17 13:22:01 +02:00
Kamil Mysliwiec
66da12aecf Merge pull request #9586 from nestjs/fix/propagate-kafka-errors
feat(microservices): add kafka retriable exception, auto-unwrap payloads
2022-05-17 13:21:35 +02:00
Kamil Myśliwiec
1f5a5b4f2e chore(): set minimum node version to v12 2022-05-17 13:21:25 +02:00
Kamil Myśliwiec
be1a8c33bd Merge branch 'Dzixxx-support-multiple-patterns' into 9.0.0 2022-05-17 13:17:02 +02:00
Kamil Myśliwiec
e3e3ef5cc5 chore(): resolve conflicts 2022-05-17 13:16:46 +02:00
Kamil Mysliwiec
fc95619e4d Merge pull request #9534 from nestjs/feat/module-utils
feat(core): add configurable module builder, module utils
2022-05-17 13:14:31 +02:00
Kamil Mysliwiec
19b55e9a37 Merge pull request #9491 from micalevisk/fix-use-isObservable
Use rxjs when checking if the value is an observable
2022-05-17 13:14:04 +02:00
Kamil Mysliwiec
ca967b809e Merge pull request #9383 from micalevisk/fix-issue-9357
feat(common)!: disallow usage of `inject` on class and value providers at type level
2022-05-17 13:13:56 +02:00
Kamil Mysliwiec
206579242a Merge pull request #9023 from ZanMinKian/fix/factory-provider-type
fix: fix factory provider definition
2022-05-17 13:13:22 +02:00
Kamil Mysliwiec
ddd24f970d Merge pull request #8058 from diesal11/master
Improve ParseUUIDPipe error message
2022-05-17 13:09:41 +02:00
Kamil Mysliwiec
28afaa27dc Merge pull request #8459 from titivuk/fix/ParseUUIDPipe
fix(common): ParseUUIDPipe - throw exceptions with exceptionFactory only
2022-05-17 13:09:16 +02:00
Kamil Myśliwiec
dde23b1a2b fix(microservices): revert interceptors (grpc option) 2022-05-17 13:05:26 +02:00
Kamil Myśliwiec
4ca738dddf chore(): resolve conflicts 2022-05-17 12:55:34 +02:00
Kamil Myśliwiec
5464d72738 chore(microservices): make method name somewhat more self descriptive 2022-05-17 12:48:29 +02:00
Kamil Myśliwiec
cfe1191d22 fix(microservices): combine streams instead of subscribing twice 2022-05-17 12:47:42 +02:00
Kamil Myśliwiec
90af9590f5 feat(microservices): dont swallow kafka event handler exceptions 2022-05-17 11:33:42 +02:00
Kamil Myśliwiec
6547f922fb feat(microservices): add kafka request deserializer, unwrap request 2022-05-17 10:49:51 +02:00
Kamil Myśliwiec
a439055646 feat(microservices): add kafka retriable exception, rethrow if needed 2022-05-17 10:41:08 +02:00
Alexander Friesen
e7087adcb7 chore: update package-lock 2022-05-07 13:15:26 +02:00
Alex
a3cfb1717a Merge branch 'master' into master 2022-05-07 13:10:53 +02:00
Kamil Myśliwiec
4d6216695d chore(): cleanup, remove initialize function calls 2022-05-06 18:57:03 +02:00
Alexander Friesen
0d0a6c7657 chore: define optional deps 2022-05-04 20:23:16 +02:00
Alexander Friesen
87969fba5f chore: update interfaces 2022-05-03 20:00:45 +02:00
Alexander Friesen
c53f7f2cde chore: update package-lock 2022-05-01 13:07:32 +02:00
Alexander Friesen
b11959c979 chore: update fastify 2022-04-30 00:47:08 +02:00
Micael Levi (@micalevisk)
bc0a1dedf8 fix(core,websockets): use rxjs when checking if is an observable 2022-04-24 15:42:14 -04:00
Micael Levi (@micalevisk)
d51c7289a8 test(websockets): add odd input on pickResult method 2022-04-24 15:42:14 -04:00
Micael Levi (@micalevisk)
41850c0920 test(core): add odd input on transformToResult method 2022-04-24 15:42:14 -04:00
Micael Levi (@micalevisk)
eb33fe5919 feat(common): disallow option inject on class/value providers types 2022-03-25 16:18:36 -04:00
Kamil Myśliwiec
c4887a7f44 chore(common): final improvements, drop initialize method 2022-03-25 14:07:22 +01:00
Kamil Myśliwiec
18be1e1158 Merge branch 'master' into feat/module-utils 2022-03-24 10:51:38 +01:00
Kamil Myśliwiec
17d48f3cb4 Merge branch 'master' into feat/module-utils 2022-03-21 15:49:30 +01:00
David Schütte
37d83eeb21 test(microservices): add tests for kafka-rpc-proxy 2022-03-10 12:36:48 +01:00
David Schütte
0b93404bef refactor(microservices): simplify function call 2022-03-10 12:34:52 +01:00
woobottle
f5051727ea fix(core): Change ReflectMetadata Params Order
i saw reflectMetadata public method in scanner.ts use params backwards
same levele methods in scanner.ts like insertInjectable,
insertExportedProvider, insertController use pararms forward
i think this params order should be fixed
and only params reversed order in reflectMetadata can get hard to read
2022-03-09 17:59:47 +09:00
Michał Dzik
5e52a7e85e chore: align message pattern to event pattern 2022-03-08 21:16:37 +01:00
Michał Dzik
0fe67d5f90 chore: alignments after review 2022-03-07 21:18:56 +01:00
titivuk
ef70418627 fix(common): ParseUUIDPipe - use optional chaining 2022-03-07 18:01:42 +01:00
David Schütte
b91803c671 feat(microservices): add consumer getter to kafka context
Provide access to native consumer of kafkajs using @Ctx() decorator.
2022-03-07 14:41:31 +01:00
Michał Dzik
48dca4ee22 chore: align event pattern 2022-03-04 14:19:02 +01:00
David Schütte
c229a75c43 fix(microservices): fix import path 2022-03-04 09:42:35 +01:00
David Schütte
77b7bd6905 feat(microservices): dont catch errors thrown by kafka event handling
Error thrown by event handling method are no longer being caught by RcpExecptionFilter.
Instead errors are passed to kafkajs's eachMessage.
This results in proper interaction with kafka.

This commit closes Kafka commitOffsets #9283.
2022-03-04 01:21:41 +01:00
David Schütte
5b24f81cd8 feat(microservices): add commit offset to kafka context
Provide access to kafkajs commitOffset by using @Ctx() decorator when handeling an event.

This commit closes Kafka commitOffsets #9283.
2022-03-04 01:17:13 +01:00
Michał Dzik
91178c8a8a chore: apply more tests 2022-02-20 14:55:51 +01:00
Michał Dzik
4ae2ef54d4 feat(microservices): add support for multiple message pattern decorator 2022-02-19 22:16:49 +01:00
Kamil Myśliwiec
bac34473d0 feat(core): add configurable module builder, module utils 2022-02-10 11:02:38 +01:00
zmj
ce96a0ed13 fix: fix factory provider definition 2022-01-25 12:02:46 +08:00
titivuk
9c0cdd2950 fix(common): ParseUUIDPipe - use isString() util function 2021-12-29 17:35:27 +01:00
titivuk
7cc81067fe fix(common): ParseUUIDPipe - make uuid regexp dictionary protected static property 2021-12-29 17:32:04 +01:00
titivuk
d32e8dd582 fix(common): ParseUUIDPipe - throw exceptions with exceptionFactory only 2021-12-29 17:32:04 +01:00
Dylan Lundy
3827736d96 Improve ParseUUIDPipe error message 2021-09-10 00:04:37 +09:30
77 changed files with 1956 additions and 1365 deletions

View File

@@ -65,11 +65,6 @@ jobs:
- store_artifacts:
path: coverage
test_node_10:
<<: *unit-tests-template
docker:
- image: circleci/node:10
test_node_14:
<<: *unit-tests-template
docker:
@@ -174,9 +169,6 @@ workflows:
- test_node_12:
requires:
- build
- test_node_10:
requires:
- build
- lint:
requires:
- build

View File

@@ -34,7 +34,7 @@ describe('Disconnected client', () => {
.send({
transport: Transport.REDIS,
options: {
url: 'redis://localhost:3333',
port: '3333',
},
})
.expect(408);

View File

@@ -3,7 +3,7 @@ import {
Controller,
InternalServerErrorException,
Post,
RequestTimeoutException,
RequestTimeoutException
} from '@nestjs/common';
import { ClientProxyFactory } from '@nestjs/microservices';
import { Observable, throwError } from 'rxjs';
@@ -24,7 +24,8 @@ export class DisconnectedClientController {
return throwError(() =>
code === 'ECONNREFUSED' ||
code === 'CONN_ERR' ||
code === 'CONNECTION_REFUSED'
code === 'CONNECTION_REFUSED' ||
error.message === 'Connection is closed.'
? new RequestTimeoutException('ECONNREFUSED')
: new InternalServerErrorException(),
);

View File

@@ -0,0 +1,17 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { IntegrationModuleOptions } from './interfaces/integration-module-options.interface';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<IntegrationModuleOptions>()
.setClassMethodName('forRoot')
.setFactoryMethodName('construct')
.setExtras(
{
isGlobal: true,
},
(definition, extras) => ({
...definition,
global: extras.isGlobal,
}),
)
.build();

View File

@@ -0,0 +1,16 @@
import { Inject, Module } from '@nestjs/common';
import {
ConfigurableModuleClass,
MODULE_OPTIONS_TOKEN,
} from './integration.module-definition';
import { IntegrationModuleOptions } from './interfaces/integration-module-options.interface';
@Module({})
export class IntegrationModule extends ConfigurableModuleClass {
constructor(
@Inject(MODULE_OPTIONS_TOKEN)
public readonly options: IntegrationModuleOptions,
) {
super();
}
}

View File

@@ -0,0 +1,4 @@
export interface IntegrationModuleOptions {
url: string;
secure?: boolean;
}

View File

@@ -0,0 +1,47 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { IntegrationModule } from '../src/integration.module';
describe('Module utils (ConfigurableModuleBuilder)', () => {
it('should auto-generate "forRoot" method', async () => {
const moduleRef = await Test.createTestingModule({
imports: [
IntegrationModule.forRoot({
isGlobal: true,
url: 'test_url',
secure: false,
}),
],
}).compile();
const integrationModule = moduleRef.get(IntegrationModule);
expect(integrationModule.options).to.deep.equal({
url: 'test_url',
secure: false,
});
});
it('should auto-generate "forRootAsync" method', async () => {
const moduleRef = await Test.createTestingModule({
imports: [
IntegrationModule.forRootAsync({
isGlobal: true,
useFactory: () => {
return {
url: 'test_url',
secure: false,
};
},
}),
],
}).compile();
const integrationModule = moduleRef.get(IntegrationModule);
expect(integrationModule.options).to.deep.equal({
url: 'test_url',
secure: false,
});
});
});

View 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",
]
}

657
package-lock.json generated
View File

@@ -1221,12 +1221,225 @@
"ajv": "^6.12.6"
}
},
"@fastify/busboy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.0.0.tgz",
"integrity": "sha512-tzTXX1TFEjWCseEsNdIlXXkD+48uJoN+zpqIojUX4pSoMscsbhO/UuVEB5SzJucexqDWOo2ma0ECwdD7hZdrzg==",
"dev": true,
"requires": {
"text-decoding": "^1.0.0"
}
},
"@fastify/cors": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-7.0.0.tgz",
"integrity": "sha512-nlo6ScwagBNJacAZD3KX90xjWLIoV0vN9QqoX1wUE9ZeZMdvkVkMZCGlxEtr00NshV0X5wDge4w5rwox7rRzSg==",
"dev": true,
"requires": {
"fastify-plugin": "^3.0.0",
"vary": "^1.1.2"
}
},
"@fastify/error": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-2.0.0.tgz",
"integrity": "sha512-wI3fpfDT0t7p8E6dA2eTECzzOd+bZsZCJ2Hcv+Onn2b7ZwK3RwD27uW2QDaMtQhAfWQQP+WNK7nKf0twLsBf9w==",
"dev": true
},
"@fastify/formbody": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-6.0.0.tgz",
"integrity": "sha512-YzPTXJbB3CzDMqU5K9YGSBt/nc/RDFZSxSWZ4SoqA3T2VRJzCPd7sZFpggdmlBRWhEBlvl0EWW7EX33kfbbFlg==",
"dev": true,
"requires": {
"fastify-plugin": "^3.0.0"
}
},
"@fastify/multipart": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-6.0.0.tgz",
"integrity": "sha512-TwxPH9jE3bEaCdMD1Xqm2YS1aelgJxcNmA/uYAPCzqnVEylDiKCmxCstGulb1W5WdMoyqD5LBGm7AoqDwWTCWQ==",
"dev": true,
"requires": {
"@fastify/busboy": "^1.0.0",
"@fastify/error": "^2.0.0",
"deepmerge": "^4.2.2",
"end-of-stream": "^1.4.4",
"fastify-plugin": "^3.0.0",
"hexoid": "^1.0.0",
"secure-json-parse": "^2.4.0",
"stream-wormhole": "^1.1.0"
},
"dependencies": {
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
}
}
},
"@fastify/static": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-5.0.0.tgz",
"integrity": "sha512-GGltJkO0idXa7yCZ0PfdTZ6qokWDX/vigCvmRpjOU2A3jc93c9p+oHDvHmwHK60hwWoBEGqHjGofVyn3H1CjZg==",
"dev": true,
"requires": {
"content-disposition": "^0.5.3",
"encoding-negotiator": "^2.0.1",
"fastify-plugin": "^3.0.0",
"glob": "^7.1.4",
"p-limit": "^3.1.0",
"readable-stream": "^3.4.0",
"send": "^0.17.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
},
"dependencies": {
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
}
}
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
"dev": true
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"http-errors": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
"dev": true,
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.1"
},
"dependencies": {
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
}
}
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
"send": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
"integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
"dev": true,
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "1.8.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
}
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"requires": {
"safe-buffer": "~5.2.0"
}
}
}
},
"@gar/promisify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz",
@@ -1494,6 +1707,12 @@
"integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==",
"dev": true
},
"@ioredis/commands": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz",
"integrity": "sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg==",
"dev": true
},
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -2780,15 +2999,6 @@
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
"dev": true
},
"@types/redis": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.11.tgz",
"integrity": "sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==",
"dev": true,
"requires": {
"redis": "*"
}
},
"@types/reflect-metadata": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@types/reflect-metadata/-/reflect-metadata-0.1.0.tgz",
@@ -5849,6 +6059,12 @@
}
}
},
"cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==",
"dev": true
},
"code-block-writer": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.0.tgz",
@@ -8449,6 +8665,12 @@
}
}
},
"encoding-negotiator": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/encoding-negotiator/-/encoding-negotiator-2.0.1.tgz",
"integrity": "sha512-GSK7qphNR4iPcejfAlZxKDoz3xMhnspwImK+Af5WhePS9jUpK/Oh7rUdyENWu+9rgDflOCTmAojBsgsvM8neAQ==",
"dev": true
},
"end-of-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@@ -9930,297 +10152,12 @@
}
}
},
"fastify-cors": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/fastify-cors/-/fastify-cors-6.1.0.tgz",
"integrity": "sha512-QBKz32IoY/iuT74CunRY1XOSpjSTIOh9E3FxulXIBhd0D2vdgG0kDvy0eG6HA/88sRfWHeba43LkGEXPz0Rh8g==",
"dev": true,
"requires": {
"fastify-cors-deprecated": "npm:fastify-cors@6.0.3",
"process-warning": "^1.0.0"
},
"dependencies": {
"fastify-cors-deprecated": {
"version": "npm:fastify-cors@6.0.3",
"resolved": "https://registry.npmjs.org/fastify-cors/-/fastify-cors-6.0.3.tgz",
"integrity": "sha512-fMbXubKKyBHHCfSBtsCi3+7VyVRdhJQmGes5gM+eGKkRErCdm0NaYO0ozd31BQBL1ycoTIjbqOZhJo4RTF/Vlg==",
"dev": true,
"requires": {
"fastify-plugin": "^3.0.0",
"vary": "^1.1.2"
}
}
}
},
"fastify-formbody": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/fastify-formbody/-/fastify-formbody-5.3.0.tgz",
"integrity": "sha512-7cjFV2HE/doojyfTwCLToIFD6Hmbw2jVTbfqZ2lbUZznQWlSXu+MBQgqBU8T2nHcMfqSi9vx6PyX0LwTehuKkg==",
"dev": true,
"requires": {
"fastify-formbody-deprecated": "npm:fastify-formbody@5.2.0",
"process-warning": "^1.0.0"
},
"dependencies": {
"fastify-formbody-deprecated": {
"version": "npm:fastify-formbody@5.2.0",
"resolved": "https://registry.npmjs.org/fastify-formbody/-/fastify-formbody-5.2.0.tgz",
"integrity": "sha512-d8Y5hCL82akPyoFiXh2wYOm3es0pV9jqoPo3pO9OV2cNF0cQx39J5WAVXzCh4MSt9Z2qF4Fy5gHlvlyESwjtvg==",
"dev": true,
"requires": {
"fastify-plugin": "^3.0.0"
}
}
}
},
"fastify-multipart": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/fastify-multipart/-/fastify-multipart-5.4.0.tgz",
"integrity": "sha512-Pafy4mtcuFUnFM/t0kgCdL854KIEoDymNVdv4nD7uBfV7lBCQq/NVEuNnaNXAbuCTpeXzYRzi50lSDa9ZM838A==",
"dev": true,
"requires": {
"fastify-multipart-deprecated": "npm:fastify-multipart@5.3.1",
"process-warning": "^1.0.0"
},
"dependencies": {
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
},
"fastify-multipart-deprecated": {
"version": "npm:fastify-multipart@5.3.1",
"resolved": "https://registry.npmjs.org/fastify-multipart/-/fastify-multipart-5.3.1.tgz",
"integrity": "sha512-c2pnGfkJmiNpYqzFYT2QfBg/06AxG531O+n1elqc8YUbWPRzufdqn3yfGAIV3RA7J4Vnf7Pfvgx0iaWqaRTOVA==",
"dev": true,
"requires": {
"@fastify/busboy": "^1.0.0",
"deepmerge": "^4.2.2",
"end-of-stream": "^1.4.4",
"fastify-error": "^0.3.0",
"fastify-plugin": "^3.0.0",
"hexoid": "^1.0.0",
"secure-json-parse": "^2.4.0",
"stream-wormhole": "^1.1.0"
},
"dependencies": {
"@fastify/busboy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.0.0.tgz",
"integrity": "sha512-tzTXX1TFEjWCseEsNdIlXXkD+48uJoN+zpqIojUX4pSoMscsbhO/UuVEB5SzJucexqDWOo2ma0ECwdD7hZdrzg==",
"dev": true,
"requires": {
"text-decoding": "^1.0.0"
}
},
"fastify-error": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz",
"integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==",
"dev": true
},
"stream-wormhole": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz",
"integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==",
"dev": true
}
}
}
}
},
"fastify-plugin": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz",
"integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w==",
"dev": true
},
"fastify-static": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/fastify-static/-/fastify-static-4.7.0.tgz",
"integrity": "sha512-zZhCfJv/hkmud2qhWqpU3K9XVAuy3+IV8Tp9BC5J5U+GyA2XwoB6h8lh9GqpEIqdXOw01WyWQllV7dOWVyAlXg==",
"dev": true,
"requires": {
"fastify-static-deprecated": "npm:fastify-static@4.6.1",
"process-warning": "^1.0.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
},
"dependencies": {
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
}
}
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
"dev": true
},
"fastify-static-deprecated": {
"version": "npm:fastify-static@4.6.1",
"resolved": "https://registry.npmjs.org/fastify-static/-/fastify-static-4.6.1.tgz",
"integrity": "sha512-vy7N28U4AMhuOim12ZZWHulEE6OQKtzZbHgiB8Zj4llUuUQXPka0WHAQI3njm1jTCx4W6fixUHfpITxweMtAIA==",
"dev": true,
"requires": {
"content-disposition": "^0.5.3",
"encoding-negotiator": "^2.0.1",
"fastify-plugin": "^3.0.0",
"glob": "^7.1.4",
"p-limit": "^3.1.0",
"readable-stream": "^3.4.0",
"send": "^0.17.1"
},
"dependencies": {
"encoding-negotiator": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/encoding-negotiator/-/encoding-negotiator-2.0.1.tgz",
"integrity": "sha512-GSK7qphNR4iPcejfAlZxKDoz3xMhnspwImK+Af5WhePS9jUpK/Oh7rUdyENWu+9rgDflOCTmAojBsgsvM8neAQ==",
"dev": true
}
}
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"http-errors": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
"dev": true,
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.1"
},
"dependencies": {
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true
}
}
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
"send": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
"integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
"dev": true,
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "1.8.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
}
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"requires": {
"safe-buffer": "~5.2.0"
}
}
}
},
"fastq": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz",
@@ -11962,12 +11899,6 @@
"integrity": "sha512-hvyIS71vs4Tu/yUYHPvGXsTgo0t3arU820+lT5VjZS2go0ewp2LqyCgxEN56CzOG7Iys52eRhHBiD1gGRdiQtw==",
"dev": true
},
"growl": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
"dev": true
},
"gulp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
@@ -13216,6 +13147,46 @@
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
"dev": true
},
"ioredis": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.0.4.tgz",
"integrity": "sha512-qFJw3MnPNsJF1lcIOP3vztbsasOXK3nDdNAgjQj7t7/Bn/w10PGchTOpqylQNxjzPbLoYDu34LjeJtSWiKBntQ==",
"dev": true,
"requires": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.0.1",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"dependencies": {
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
},
"denque": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz",
"integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==",
"dev": true
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@@ -15187,6 +15158,12 @@
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=",
"dev": true
},
"lodash.escape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz",
@@ -16282,32 +16259,30 @@
}
},
"mocha": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz",
"integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==",
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz",
"integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==",
"dev": true,
"requires": {
"@ungap/promise-all-settled": "1.1.2",
"ansi-colors": "4.1.1",
"browser-stdout": "1.3.1",
"chokidar": "3.5.3",
"debug": "4.3.3",
"debug": "4.3.4",
"diff": "5.0.0",
"escape-string-regexp": "4.0.0",
"find-up": "5.0.0",
"glob": "7.2.0",
"growl": "1.10.5",
"he": "1.2.0",
"js-yaml": "4.1.0",
"log-symbols": "4.1.0",
"minimatch": "4.2.1",
"minimatch": "5.0.1",
"ms": "2.1.3",
"nanoid": "3.3.1",
"nanoid": "3.3.3",
"serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1",
"supports-color": "8.1.1",
"which": "2.0.2",
"workerpool": "6.2.0",
"workerpool": "6.2.1",
"yargs": "16.2.0",
"yargs-parser": "20.2.4",
"yargs-unparser": "2.0.0"
@@ -16412,9 +16387,9 @@
"dev": true
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
@@ -16550,12 +16525,23 @@
}
},
"minimatch": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz",
"integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
"brace-expansion": "^2.0.1"
},
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0"
}
}
}
},
"ms": {
@@ -16627,15 +16613,6 @@
"is-number": "^7.0.0"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
},
"yargs-parser": {
"version": "20.2.4",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
@@ -17031,9 +17008,9 @@
"optional": true
},
"nanoid": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
"integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
"dev": true
},
"nanomatch": {
@@ -20765,6 +20742,12 @@
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=",
"dev": true
},
"standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"dev": true
},
"static-eval": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz",
@@ -20894,6 +20877,12 @@
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
"dev": true
},
"stream-wormhole": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz",
"integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==",
"dev": true
},
"streamsearch": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
@@ -21594,6 +21583,12 @@
"through2": "^2.0.3"
}
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true
},
"touch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
@@ -22806,9 +22801,9 @@
"dev": true
},
"workerpool": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz",
"integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
"integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
"dev": true
},
"wrap-ansi": {

View File

@@ -72,6 +72,10 @@
"@codechecks/client": "0.1.12",
"@commitlint/cli": "17.0.0",
"@commitlint/config-angular": "17.0.0",
"@fastify/cors": "7.0.0",
"@fastify/formbody": "6.0.0",
"@fastify/multipart": "6.0.0",
"@fastify/static": "5.0.0",
"@grpc/grpc-js": "1.6.7",
"@grpc/proto-loader": "0.6.12",
"@nestjs/apollo": "10.0.11",
@@ -89,7 +93,6 @@
"@types/http-errors": "1.8.2",
"@types/mocha": "9.1.1",
"@types/node": "17.0.34",
"@types/redis": "4.0.11",
"@types/reflect-metadata": "0.1.0",
"@types/sinon": "10.0.11",
"@types/socket.io": "3.0.2",
@@ -122,10 +125,6 @@
"eventsource": "2.0.2",
"fancy-log": "2.0.0",
"fastify": "3.29.0",
"fastify-cors": "6.1.0",
"fastify-formbody": "5.3.0",
"fastify-multipart": "5.4.0",
"fastify-static": "4.7.0",
"graphql": "15.8.0",
"graphql-tools": "8.2.9",
"gulp": "4.0.2",
@@ -137,6 +136,7 @@
"http-errors": "2.0.0",
"husky": "8.0.1",
"imports-loader": "3.1.1",
"ioredis": "5.0.4",
"json-loader": "0.5.7",
"kafkajs": "2.0.0",
"lerna": "3.0.0",
@@ -146,7 +146,7 @@
"markdown-table": "2.0.0",
"merge-graphql-schemas": "1.7.8",
"middie": "6.1.0",
"mocha": "9.2.2",
"mocha": "10.0.0",
"mongoose": "6.3.3",
"mqtt": "4.3.7",
"multer": "1.4.4",
@@ -156,7 +156,6 @@
"nyc": "15.1.0",
"point-of-view": "5.3.0",
"prettier": "2.6.2",
"redis": "3.1.2",
"rxjs-compat": "6.6.7",
"sinon": "14.0.0",
"sinon-chai": "3.7.0",
@@ -171,7 +170,7 @@
"ws": "8.6.0"
},
"engines": {
"node": ">= 10.13.0"
"node": ">= 12.9.0"
},
"collective": {
"type": "opencollective",

View File

@@ -1,4 +1,3 @@
export const CACHE_MANAGER = 'CACHE_MANAGER';
export const CACHE_MODULE_OPTIONS = 'CACHE_MODULE_OPTIONS';
export const CACHE_KEY_METADATA = 'cache_module:cache_key';
export const CACHE_TTL_METADATA = 'cache_module:cache_ttl';

View File

@@ -0,0 +1,12 @@
import { ConfigurableModuleBuilder } from '../module-utils';
import {
CacheModuleOptions,
CacheOptionsFactory,
} from './interfaces/cache-module.interface';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<CacheModuleOptions>({
moduleName: 'Cache',
})
.setFactoryMethodName('createCacheOptions' as keyof CacheOptionsFactory)
.build();

View File

@@ -1,11 +1,11 @@
import { Module } from '../decorators';
import { DynamicModule, Provider } from '../interfaces';
import { CACHE_MANAGER, CACHE_MODULE_OPTIONS } from './cache.constants';
import { DynamicModule } from '../interfaces';
import { CACHE_MANAGER } from './cache.constants';
import { ConfigurableModuleClass } from './cache.module-definition';
import { createCacheManager } from './cache.providers';
import {
CacheModuleAsyncOptions,
CacheModuleOptions,
CacheOptionsFactory,
} from './interfaces/cache-module.interface';
/**
@@ -19,7 +19,7 @@ import {
providers: [createCacheManager()],
exports: [CACHE_MANAGER],
})
export class CacheModule {
export class CacheModule extends ConfigurableModuleClass {
/**
* Configure the cache manager statically.
*
@@ -31,9 +31,8 @@ export class CacheModule {
options: CacheModuleOptions<StoreConfig> = {} as any,
): DynamicModule {
return {
module: CacheModule,
global: options.isGlobal,
providers: [{ provide: CACHE_MODULE_OPTIONS, useValue: options }],
...super.register(options),
};
}
@@ -48,47 +47,11 @@ export class CacheModule {
static registerAsync<
StoreConfig extends Record<any, any> = Record<string, any>,
>(options: CacheModuleAsyncOptions<StoreConfig>): DynamicModule {
const moduleDefinition = super.registerAsync(options);
return {
module: CacheModule,
global: options.isGlobal,
imports: options.imports,
providers: [
...this.createAsyncProviders<StoreConfig>(options),
...(options.extraProviders || []),
],
};
}
private static createAsyncProviders<StoreConfig extends Record<any, any>>(
options: CacheModuleAsyncOptions<StoreConfig>,
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
}
return [
this.createAsyncOptionsProvider(options),
{
provide: options.useClass,
useClass: options.useClass,
},
];
}
private static createAsyncOptionsProvider<
StoreConfig extends Record<any, any>,
>(options: CacheModuleAsyncOptions<StoreConfig>): Provider {
if (options.useFactory) {
return {
provide: CACHE_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
return {
provide: CACHE_MODULE_OPTIONS,
useFactory: async (optionsFactory: CacheOptionsFactory<StoreConfig>) =>
optionsFactory.createCacheOptions(),
inject: [options.useExisting || options.useClass],
...moduleDefinition,
providers: moduleDefinition.providers.concat(options.extraProviders),
};
}
}

View File

@@ -1,6 +1,7 @@
import { Provider } from '../interfaces';
import { loadPackage } from '../utils/load-package.util';
import { CACHE_MANAGER, CACHE_MODULE_OPTIONS } from './cache.constants';
import { CACHE_MANAGER } from './cache.constants';
import { MODULE_OPTIONS_TOKEN } from './cache.module-definition';
import { defaultCacheOptions } from './default-options';
import { CacheManagerOptions } from './interfaces/cache-manager.interface';
@@ -31,6 +32,6 @@ export function createCacheManager(): Provider {
...(options || {}),
});
},
inject: [CACHE_MODULE_OPTIONS],
inject: [MODULE_OPTIONS_TOKEN],
};
}

View File

@@ -1,4 +1,5 @@
import { ModuleMetadata, Provider, Type } from '../../interfaces';
import { Provider, Type } from '../../interfaces';
import { ConfigurableModuleAsyncOptions } from '../../module-utils';
import { CacheManagerOptions } from './cache-manager.interface';
export type CacheModuleOptions<
@@ -39,7 +40,10 @@ export interface CacheOptionsFactory<
*/
export interface CacheModuleAsyncOptions<
StoreConfig extends Record<any, any> = Record<string, any>,
> extends Pick<ModuleMetadata, 'imports'> {
> extends ConfigurableModuleAsyncOptions<
CacheModuleOptions<StoreConfig>,
keyof CacheOptionsFactory
> {
/**
* Injection token resolving to an existing provider. The provider must implement
* the `CacheOptionsFactory` interface.
@@ -63,6 +67,9 @@ export interface CacheModuleAsyncOptions<
* Dependencies that a Factory may inject.
*/
inject?: any[];
/**
* Extra providers to be registered within a scope of this module.
*/
extraProviders?: Provider[];
/**
* If "true', register `CacheModule` as a global module.

View File

@@ -1,3 +0,0 @@
export const AXIOS_INSTANCE_TOKEN = 'AXIOS_INSTANCE_TOKEN';
export const HTTP_MODULE_ID = 'HTTP_MODULE_ID';
export const HTTP_MODULE_OPTIONS = 'HTTP_MODULE_OPTIONS';

View File

@@ -1,99 +0,0 @@
import Axios from 'axios';
import { Module } from '../decorators/modules/module.decorator';
import { DynamicModule, Provider } from '../interfaces';
import { randomStringGenerator } from '../utils/random-string-generator.util';
import {
AXIOS_INSTANCE_TOKEN,
HTTP_MODULE_ID,
HTTP_MODULE_OPTIONS,
} from './http.constants';
import { HttpService } from './http.service';
import {
HttpModuleAsyncOptions,
HttpModuleOptions,
HttpModuleOptionsFactory,
} from './interfaces';
/**
* @deprecated "HttpModule" (from the "@nestjs/common" package) is deprecated and will be removed in the next major release. Please, use the "@nestjs/axios" package instead.
*/
@Module({
providers: [
HttpService,
{
provide: AXIOS_INSTANCE_TOKEN,
useValue: Axios,
},
],
exports: [HttpService],
})
export class HttpModule {
static register(config: HttpModuleOptions): DynamicModule {
return {
module: HttpModule,
providers: [
{
provide: AXIOS_INSTANCE_TOKEN,
useValue: Axios.create(config),
},
{
provide: HTTP_MODULE_ID,
useValue: randomStringGenerator(),
},
],
};
}
static registerAsync(options: HttpModuleAsyncOptions): DynamicModule {
return {
module: HttpModule,
imports: options.imports,
providers: [
...this.createAsyncProviders(options),
{
provide: AXIOS_INSTANCE_TOKEN,
useFactory: (config: HttpModuleOptions) => Axios.create(config),
inject: [HTTP_MODULE_OPTIONS],
},
{
provide: HTTP_MODULE_ID,
useValue: randomStringGenerator(),
},
...(options.extraProviders || []),
],
};
}
private static createAsyncProviders(
options: HttpModuleAsyncOptions,
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
}
return [
this.createAsyncOptionsProvider(options),
{
provide: options.useClass,
useClass: options.useClass,
},
];
}
private static createAsyncOptionsProvider(
options: HttpModuleAsyncOptions,
): Provider {
if (options.useFactory) {
return {
provide: HTTP_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
return {
provide: HTTP_MODULE_OPTIONS,
useFactory: async (optionsFactory: HttpModuleOptionsFactory) =>
optionsFactory.createHttpOptions(),
inject: [options.useExisting || options.useClass],
};
}
}

View File

@@ -1,113 +0,0 @@
import Axios, {
AxiosInstance,
AxiosPromise,
AxiosRequestConfig,
AxiosResponse,
CancelTokenSource,
} from 'axios';
import { Observable } from 'rxjs';
import { Inject } from '../decorators';
import { Logger } from '../services';
import { AXIOS_INSTANCE_TOKEN } from './http.constants';
/**
* @deprecated "HttpModule" (from the "@nestjs/common" package) is deprecated and will be removed in the next major release. Please, use the "@nestjs/axios" package instead.
*/
export class HttpService {
private readonly logger = new Logger(HttpService.name);
constructor(
@Inject(AXIOS_INSTANCE_TOKEN)
private readonly instance: AxiosInstance = Axios,
) {
this.logger.warn(
'DEPRECATED! "HttpModule" (from the "@nestjs/common" package) is deprecated and will be removed in the next major release. Please, use the "@nestjs/axios" package instead.',
);
}
request<T = any>(config: AxiosRequestConfig): Observable<AxiosResponse<T>> {
return this.makeObservable<T>(this.instance.request, config);
}
get<T = any>(
url: string,
config?: AxiosRequestConfig,
): Observable<AxiosResponse<T>> {
return this.makeObservable<T>(this.instance.get, url, config);
}
delete<T = any>(
url: string,
config?: AxiosRequestConfig,
): Observable<AxiosResponse<T>> {
return this.makeObservable<T>(this.instance.delete, url, config);
}
head<T = any>(
url: string,
config?: AxiosRequestConfig,
): Observable<AxiosResponse<T>> {
return this.makeObservable<T>(this.instance.head, url, config);
}
post<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig,
): Observable<AxiosResponse<T>> {
return this.makeObservable<T>(this.instance.post, url, data, config);
}
put<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig,
): Observable<AxiosResponse<T>> {
return this.makeObservable<T>(this.instance.put, url, data, config);
}
patch<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig,
): Observable<AxiosResponse<T>> {
return this.makeObservable<T>(this.instance.patch, url, data, config);
}
get axiosRef(): AxiosInstance {
return this.instance;
}
private makeObservable<T>(
axios: (...args: any[]) => AxiosPromise<T>,
...args: any[]
) {
return new Observable<AxiosResponse<T>>(subscriber => {
const config: AxiosRequestConfig = { ...(args[args.length - 1] || {}) };
let cancelSource: CancelTokenSource;
if (!config.cancelToken) {
cancelSource = Axios.CancelToken.source();
config.cancelToken = cancelSource.token;
}
axios(...args)
.then(res => {
subscriber.next(res);
subscriber.complete();
})
.catch(err => {
subscriber.error(err);
});
return () => {
if (config.responseType === 'stream') {
return;
}
if (cancelSource) {
cancelSource.cancel();
}
};
});
}
}

View File

@@ -1,3 +0,0 @@
export * from './http.module';
export * from './http.service';
export * from './interfaces';

View File

@@ -1,19 +0,0 @@
import { AxiosRequestConfig } from 'axios';
import { ModuleMetadata, Provider, Type } from '../../interfaces';
export type HttpModuleOptions = AxiosRequestConfig;
export interface HttpModuleOptionsFactory {
createHttpOptions(): Promise<HttpModuleOptions> | HttpModuleOptions;
}
export interface HttpModuleAsyncOptions
extends Pick<ModuleMetadata, 'imports'> {
useExisting?: Type<HttpModuleOptionsFactory>;
useClass?: Type<HttpModuleOptionsFactory>;
useFactory?: (
...args: any[]
) => Promise<HttpModuleOptions> | HttpModuleOptions;
inject?: any[];
extraProviders?: Provider[];
}

View File

@@ -1,2 +0,0 @@
export * from './http-module.interface';
export * from './raw-body-request.interface';

View File

@@ -11,7 +11,6 @@ export * from './decorators';
export * from './enums';
export * from './exceptions';
export * from './file-stream';
export * from './http';
export {
Abstract,
ArgumentMetadata,
@@ -49,6 +48,7 @@ export {
Paramtype,
PipeTransform,
Provider,
RawBodyRequest,
RpcExceptionFilter,
Scope,
ScopeOptions,
@@ -61,6 +61,7 @@ export {
WsExceptionFilter,
WsMessageHandler,
} from './interfaces';
export * from './module-utils';
export * from './pipes';
export * from './serializer';
export * from './services';

View File

@@ -1,2 +1,3 @@
export * from './http-server.interface';
export * from './message-event.interface';
export * from './raw-body-request.interface';

View File

@@ -46,6 +46,12 @@ export interface ClassProvider<T = any> {
* Optional enum defining lifetime of the provider that is injected.
*/
scope?: Scope;
/**
* This option is only available on factory providers!
*
* @see [Use factory](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory)
*/
inject?: never;
}
/**
@@ -72,6 +78,12 @@ export interface ValueProvider<T = any> {
* Instance of a provider to be injected.
*/
useValue: T;
/**
* This option is only available on factory providers!
*
* @see [Use factory](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory)
*/
inject?: never;
}
/**
@@ -102,7 +114,7 @@ export interface FactoryProvider<T = any> {
/**
* Factory function that returns an instance of the provider to be injected.
*/
useFactory: (...args: any[]) => T;
useFactory: (...args: any[]) => T | Promise<T>;
/**
* Optional list of providers to be injected into the context of the Factory function.
*/

View File

@@ -0,0 +1,330 @@
import { DynamicModule, Provider } from '../interfaces';
import { Logger } from '../services/logger.service';
import { randomStringGenerator } from '../utils/random-string-generator.util';
import {
ASYNC_METHOD_SUFFIX,
CONFIGURABLE_MODULE_ID,
DEFAULT_FACTORY_CLASS_METHOD_KEY,
DEFAULT_METHOD_KEY,
} from './constants';
import {
ConfigurableModuleAsyncOptions,
ConfigurableModuleCls,
ConfigurableModuleOptionsFactory,
} from './interfaces';
import { ConfigurableModuleHost } from './interfaces/configurable-module-host.interface';
import { generateOptionsInjectionToken } from './utils/generate-options-injection-token.util';
/**
* @publicApi
*/
export interface ConfigurableModuleBuilderOptions {
/**
* Specified what injection token should be used for the module options provider.
* By default, an auto-generated UUID will be used.
*/
optionsInjectionToken?: string | symbol;
/**
* By default, an UUID will be used as a module options provider token.
* Explicitly specifying the "moduleName" will instruct the "ConfigurableModuleBuilder"
* to use a more descriptive provider token.
*
* For example, if `moduleName: "Cache"` then auto-generated provider token will be "CACHE_MODULE_OPTIONS".
*/
moduleName?: string;
/**
* Indicates whether module should always be "transient", meaning,
* every time you call the static method to construct a dynamic module,
* regardless of what arguments you pass in, a new "unique" module will be created.
*
* @default false
*/
alwaysTransient?: boolean;
}
/**
* Factory that lets you create configurable modules and
* provides a way to reduce the majority of dynamic module boilerplate.
*
* @publicApi
*/
export class ConfigurableModuleBuilder<
ModuleOptions,
StaticMethodKey extends string = typeof DEFAULT_METHOD_KEY,
FactoryClassMethodKey extends string = typeof DEFAULT_FACTORY_CLASS_METHOD_KEY,
ExtraModuleDefinitionOptions = {},
> {
protected staticMethodKey: StaticMethodKey;
protected factoryClassMethodKey: FactoryClassMethodKey;
protected extras: ExtraModuleDefinitionOptions;
protected transformModuleDefinition: (
definition: DynamicModule,
extraOptions: ExtraModuleDefinitionOptions,
) => DynamicModule;
protected readonly logger = new Logger(ConfigurableModuleBuilder.name);
constructor(
protected readonly options: ConfigurableModuleBuilderOptions = {},
parentBuilder?: ConfigurableModuleBuilder<ModuleOptions>,
) {
if (parentBuilder) {
this.staticMethodKey = parentBuilder.staticMethodKey as StaticMethodKey;
this.factoryClassMethodKey =
parentBuilder.factoryClassMethodKey as FactoryClassMethodKey;
this.transformModuleDefinition = parentBuilder.transformModuleDefinition;
this.extras = parentBuilder.extras as ExtraModuleDefinitionOptions;
}
}
/**
* Registers the "extras" object (a set of extra options that can be used to modify the dynamic module definition).
* Values you specify within the "extras" object will be used as default values (that can be overriden by module consumers).
*
* This method also applies the so-called "module definition transform function" that takes the auto-generated
* dynamic module object ("DynamicModule") and the actual consumer "extras" object as input parameters.
* The "extras" object consists of values explicitly specified by module consumers and default values.
*
* @example
* ```typescript
* .setExtras<{ isGlobal?: boolean }>({ isGlobal: false }, (definition, extras) =>
* ({ ...definition, global: extras.isGlobal })
* )
* ```
*/
setExtras<ExtraModuleDefinitionOptions>(
extras: ExtraModuleDefinitionOptions,
transformDefinition: (
definition: DynamicModule,
extras: ExtraModuleDefinitionOptions,
) => DynamicModule,
) {
const builder = new ConfigurableModuleBuilder<
ModuleOptions,
StaticMethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
>(this.options, this as any);
builder.extras = extras;
builder.transformModuleDefinition = transformDefinition;
return builder;
}
/**
* Dynamic modules must expose public static methods that let you pass in
* configuration parameters (control the module's behavior from the outside).
* Some frequently used names that you may have seen in other modules are:
* "forRoot", "forFeature", "register", "configure".
*
* This method "setClassMethodName" lets you specify the name of the
* method that will be auto-generated.
*
* @param key name of the method
*/
setClassMethodName<StaticMethodKey extends string>(key: StaticMethodKey) {
const builder = new ConfigurableModuleBuilder<
ModuleOptions,
StaticMethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
>(this.options, this as any);
builder.staticMethodKey = key;
return builder;
}
/**
* Asynchronously configured modules (that rely on other modules, i.e. "ConfigModule")
* let you pass the configuration factory class that will be registered and instantiated as a provider.
* This provider then will be used to retrieve the module's configuration. To provide the configuration,
* the corresponding factory method must be implemented.
*
* This method ("setFactoryMethodName") lets you control what method name will have to be
* implemented by the config factory (default is "create").
*
* @param key name of the method
*/
setFactoryMethodName<FactoryClassMethodKey extends string>(
key: FactoryClassMethodKey,
) {
const builder = new ConfigurableModuleBuilder<
ModuleOptions,
StaticMethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
>(this.options, this as any);
builder.factoryClassMethodKey = key;
return builder;
}
/**
* Returns an object consisting of multiple properties that lets you
* easily construct dynamic configurable modules. See "ConfigurableModuleHost" interface for more details.
*/
build(): ConfigurableModuleHost<
ModuleOptions,
StaticMethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
> {
this.staticMethodKey ??= DEFAULT_METHOD_KEY as StaticMethodKey;
this.factoryClassMethodKey ??=
DEFAULT_FACTORY_CLASS_METHOD_KEY as FactoryClassMethodKey;
this.options.optionsInjectionToken ??= this.options.moduleName
? this.constructInjectionTokenString()
: generateOptionsInjectionToken();
this.transformModuleDefinition ??= definition => definition;
return {
ConfigurableModuleClass:
this.createConfigurableModuleCls<ModuleOptions>(),
MODULE_OPTIONS_TOKEN: this.options.optionsInjectionToken,
ASYNC_OPTIONS_TYPE: this.createTypeProxy('ASYNC_OPTIONS_TYPE'),
OPTIONS_TYPE: this.createTypeProxy('OPTIONS_TYPE'),
};
}
private constructInjectionTokenString(): string {
const moduleNameInSnakeCase = this.options.moduleName
.trim()
.split(/(?=[A-Z])/)
.join('_')
.toUpperCase();
return `${moduleNameInSnakeCase}_MODULE_OPTIONS`;
}
private createConfigurableModuleCls<ModuleOptions>(): ConfigurableModuleCls<
ModuleOptions,
StaticMethodKey,
FactoryClassMethodKey
> {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const asyncMethodKey = this.staticMethodKey + ASYNC_METHOD_SUFFIX;
class InternalModuleClass {
static [self.staticMethodKey](
options: ModuleOptions & ExtraModuleDefinitionOptions,
): DynamicModule {
const providers = [
{
provide: self.options.optionsInjectionToken,
useValue: this.omitExtras(options, self.extras),
},
];
if (self.options.alwaysTransient) {
providers.push({
provide: CONFIGURABLE_MODULE_ID,
useValue: randomStringGenerator(),
});
}
return self.transformModuleDefinition(
{
module: this,
providers,
},
options,
);
}
static [asyncMethodKey](
options: ConfigurableModuleAsyncOptions<ModuleOptions> &
ExtraModuleDefinitionOptions,
): DynamicModule {
const providers = this.createAsyncProviders(options);
if (self.options.alwaysTransient) {
providers.push({
provide: CONFIGURABLE_MODULE_ID,
useValue: randomStringGenerator(),
});
}
return self.transformModuleDefinition(
{
module: this,
imports: options.imports || [],
providers,
},
options,
);
}
private static omitExtras(
input: ModuleOptions & ExtraModuleDefinitionOptions,
extras: ExtraModuleDefinitionOptions | undefined,
): ModuleOptions {
if (!extras) {
return input;
}
const moduleOptions = {};
const extrasKeys = Object.keys(extras);
Object.keys(input)
.filter(key => !extrasKeys.includes(key))
.forEach(key => {
moduleOptions[key] = input[key];
});
return moduleOptions as ModuleOptions;
}
private static createAsyncProviders(
options: ConfigurableModuleAsyncOptions<ModuleOptions>,
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
}
return [
this.createAsyncOptionsProvider(options),
{
provide: options.useClass,
useClass: options.useClass,
},
];
}
private static createAsyncOptionsProvider(
options: ConfigurableModuleAsyncOptions<ModuleOptions>,
): Provider {
if (options.useFactory) {
return {
provide: self.options.optionsInjectionToken,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
return {
provide: self.options.optionsInjectionToken,
useFactory: async (
optionsFactory: ConfigurableModuleOptionsFactory<
ModuleOptions,
FactoryClassMethodKey
>,
) =>
await optionsFactory[
self.factoryClassMethodKey as keyof typeof optionsFactory
](),
inject: [options.useExisting || options.useClass],
};
}
}
return InternalModuleClass as unknown as ConfigurableModuleCls<
ModuleOptions,
StaticMethodKey,
FactoryClassMethodKey
>;
}
private createTypeProxy(
typeName: 'OPTIONS_TYPE' | 'ASYNC_OPTIONS_TYPE' | 'OptionsFactoryInterface',
) {
const proxy = new Proxy(
{},
{
get: () => {
throw new Error(
`"${typeName}" is not supposed to be used as a value.`,
);
},
},
);
return proxy as any;
}
}

View File

@@ -0,0 +1,5 @@
export const DEFAULT_METHOD_KEY = 'register';
export const DEFAULT_FACTORY_CLASS_METHOD_KEY = 'create';
export const ASYNC_METHOD_SUFFIX = 'Async';
export const CONFIGURABLE_MODULE_ID = 'CONFIGURABLE_MODULE_ID';

View File

@@ -0,0 +1,2 @@
export * from './configurable-module.builder';
export * from './interfaces';

View File

@@ -0,0 +1,51 @@
import { FactoryProvider, ModuleMetadata, Type } from '../../interfaces';
import { DEFAULT_FACTORY_CLASS_METHOD_KEY } from '../constants';
/**
* Interface that must be implemented by the module options factory class.
* Method key varies depending on the "FactoryClassMethodKey" type argument.
*
* @publicApi
*/
export type ConfigurableModuleOptionsFactory<
ModuleOptions,
FactoryClassMethodKey extends string,
> = Record<
`${FactoryClassMethodKey}`,
() => Promise<ModuleOptions> | ModuleOptions
>;
/**
* Interface that represents the module async options object
* Factory method name varies depending on the "FactoryClassMethodKey" type argument.
*
* @publicApi
*/
export interface ConfigurableModuleAsyncOptions<
ModuleOptions,
FactoryClassMethodKey extends string = typeof DEFAULT_FACTORY_CLASS_METHOD_KEY,
> extends Pick<ModuleMetadata, 'imports'> {
/**
* Injection token resolving to an existing provider. The provider must implement
* the corresponding interface.
*/
useExisting?: Type<
ConfigurableModuleOptionsFactory<ModuleOptions, FactoryClassMethodKey>
>;
/**
* Injection token resolving to a class that will be instantiated as a provider.
* The class must implement the corresponding interface.
*/
useClass?: Type<
ConfigurableModuleOptionsFactory<ModuleOptions, FactoryClassMethodKey>
>;
/**
* Function returning options (or a Promise resolving to options) to configure the
* cache module.
*/
useFactory?: (...args: unknown[]) => Promise<ModuleOptions> | ModuleOptions;
/**
* Dependencies that a Factory may inject.
*/
inject?: FactoryProvider['inject'];
}

View File

@@ -0,0 +1,35 @@
import { DynamicModule } from '../../interfaces';
import {
DEFAULT_FACTORY_CLASS_METHOD_KEY,
DEFAULT_METHOD_KEY,
} from '../constants';
import { ConfigurableModuleAsyncOptions } from './configurable-module-async-options.interface';
/**
* Class that represents a blueprint/prototype for a configurable Nest module.
* This class provides static methods for constructing dynamic modules. Their names
* can be controlled through the "MethodKey" type argument.
*
* @publicApi
*/
export type ConfigurableModuleCls<
ModuleOptions,
MethodKey extends string = typeof DEFAULT_METHOD_KEY,
FactoryClassMethodKey extends string = typeof DEFAULT_FACTORY_CLASS_METHOD_KEY,
ExtraModuleDefinitionOptions = {},
> = {
new (): any;
} & Record<
`${MethodKey}`,
(options: ModuleOptions & ExtraModuleDefinitionOptions) => DynamicModule
> &
Record<
`${MethodKey}Async`,
(
options: ConfigurableModuleAsyncOptions<
ModuleOptions,
FactoryClassMethodKey
> &
ExtraModuleDefinitionOptions,
) => DynamicModule
>;

View File

@@ -0,0 +1,77 @@
import { ConfigurableModuleAsyncOptions } from './configurable-module-async-options.interface';
import { ConfigurableModuleCls } from './configurable-module-cls.interface';
/**
* Configurable module host. See properties for more details
*
* @publicApi
*/
export interface ConfigurableModuleHost<
ModuleOptions = Record<string, unknown>,
MethodKey extends string = string,
FactoryClassMethodKey extends string = string,
ExtraModuleDefinitionOptions = {},
> {
/**
* Class that represents a blueprint/prototype for a configurable Nest module.
* This class provides static methods for constructing dynamic modules. Their names
* can be controlled through the "MethodKey" type argument.
*
* Your module class should inherit from this class to make the static methods available.
*
* @example
* ```typescript
* @Module({})
* class IntegrationModule extends ConfigurableModuleCls {
* // ...
* }
* ```
*/
ConfigurableModuleClass: ConfigurableModuleCls<
ModuleOptions,
MethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
>;
/**
* Module options provider token. Can be used to inject the "options object" to
* providers registered within the host module.
*/
MODULE_OPTIONS_TOKEN: string | symbol;
/**
* Can be used to auto-infer the compound "async module options" type.
* Note: this property is not supposed to be used as a value.
*
* @example
* ```typescript
* @Module({})
* class IntegrationModule extends ConfigurableModuleCls {
* static module = initializer(IntegrationModule);
*
* static registerAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
* return super.registerAsync(options);
* }
* ```
*/
ASYNC_OPTIONS_TYPE: ConfigurableModuleAsyncOptions<
ModuleOptions,
FactoryClassMethodKey
> &
ExtraModuleDefinitionOptions;
/**
* Can be used to auto-infer the compound "module options" type (options interface + extra module definition options).
* Note: this property is not supposed to be used as a value.
*
* @example
* ```typescript
* @Module({})
* class IntegrationModule extends ConfigurableModuleCls {
* static module = initializer(IntegrationModule);
*
* static register(options: typeof OPTIONS_TYPE): DynamicModule {
* return super.register(options);
* }
* ```
*/
OPTIONS_TYPE: ModuleOptions & ExtraModuleDefinitionOptions;
}

View File

@@ -0,0 +1,3 @@
export * from './configurable-module-async-options.interface';
export * from './configurable-module-cls.interface';
export * from './configurable-module-host.interface';

View File

@@ -0,0 +1,6 @@
import { randomStringGenerator } from '../../utils/random-string-generator.util';
export function generateOptionsInjectionToken() {
const hash = randomStringGenerator();
return `CONFIGURABLE_MODULE_OPTIONS[${hash}]`;
}

View File

@@ -9,7 +9,7 @@ import {
ErrorHttpStatusCode,
HttpErrorByCode,
} from '../utils/http-error-by-code.util';
import { isUUID } from '../utils/is-uuid';
import { isString } from '../utils/shared.utils';
export interface ParseUUIDPipeOptions {
version?: '3' | '4' | '5';
@@ -19,6 +19,12 @@ export interface ParseUUIDPipeOptions {
@Injectable()
export class ParseUUIDPipe implements PipeTransform<string> {
protected static uuidRegExps = {
3: /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i,
4: /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
5: /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
all: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i,
};
private readonly version: '3' | '4' | '5';
protected exceptionFactory: (errors: string) => any;
@@ -35,14 +41,23 @@ export class ParseUUIDPipe implements PipeTransform<string> {
exceptionFactory ||
(error => new HttpErrorByCode[errorHttpStatusCode](error));
}
async transform(value: string, metadata: ArgumentMetadata): Promise<string> {
if (!isUUID(value, this.version)) {
if (!this.isUUID(value, this.version)) {
throw this.exceptionFactory(
`Validation failed (uuid ${
this.version ? 'v' + this.version : ''
`Validation failed (uuid${
this.version ? ` v ${this.version}` : ''
} is expected)`,
);
}
return value;
}
protected isUUID(str: unknown, version = 'all') {
if (!isString(str)) {
throw this.exceptionFactory('The value passed as UUID is not a string');
}
const pattern = ParseUUIDPipe.uuidRegExps[version];
return pattern?.test(str);
}
}

View File

@@ -1,16 +0,0 @@
import { AxiosRequestConfig } from 'axios';
import { expect } from 'chai';
import { lastValueFrom } from 'rxjs';
import { HttpService } from '../../http/http.service';
describe('HttpService', () => {
it('should not mutate user-given axios options object', done => {
const http = new HttpService({ get: () => Promise.resolve() } as any);
const options: AxiosRequestConfig = {};
lastValueFrom(http.get('/', options)).then(() => {
expect(options.cancelToken).to.be.undefined;
done();
});
});
});

View File

@@ -0,0 +1,112 @@
import { expect } from 'chai';
import { Provider } from '../../interfaces';
import { ConfigurableModuleBuilder } from '../../module-utils';
describe('ConfigurableModuleBuilder', () => {
describe('setExtras', () => {
it('should apply module definition transformer function and return typed builder', () => {
const { ConfigurableModuleClass } = new ConfigurableModuleBuilder()
.setExtras(
{ isGlobal: false },
(definition, extras: { isGlobal: boolean }) => ({
...definition,
global: extras.isGlobal,
}),
)
.build();
expect(
ConfigurableModuleClass.register({
// No type error
isGlobal: true,
}),
).to.deep.include({
global: true,
});
});
});
describe('setClassMethodName', () => {
it('should set static class method name and return typed builder', () => {
const { ConfigurableModuleClass } = new ConfigurableModuleBuilder()
.setClassMethodName('forRoot')
.build();
expect(ConfigurableModuleClass.forRoot).to.not.be.undefined;
expect(ConfigurableModuleClass.forRootAsync).to.not.be.undefined;
expect((ConfigurableModuleClass as any).register).to.be.undefined;
});
});
describe('setFactoryMethodName', () => {
it('should set configuration factory class method name and return typed builder', () => {
const { ConfigurableModuleClass } = new ConfigurableModuleBuilder()
.setFactoryMethodName('createOptions')
.build();
expect(
ConfigurableModuleClass.registerAsync({
useClass: class {
// No type error
createOptions() {}
},
}),
).to.not.be.undefined;
});
});
describe('build', () => {
it('should return a fully typed "ConfigurableModuleClass"', () => {
type ExtraConfig = { isGlobal?: boolean; extraProviders: Provider[] };
const {
ConfigurableModuleClass,
OPTIONS_TYPE,
ASYNC_OPTIONS_TYPE,
MODULE_OPTIONS_TOKEN,
} = new ConfigurableModuleBuilder({
moduleName: 'RandomTest',
alwaysTransient: true,
})
.setFactoryMethodName('createOptions')
.setClassMethodName('forFeature')
.setExtras<ExtraConfig>(
{ isGlobal: false, extraProviders: [] },
(definition, extras) => ({
...definition,
global: extras.isGlobal,
providers: definition.providers?.concat(extras.extraProviders),
}),
)
.build();
const definition = ConfigurableModuleClass.forFeatureAsync({
useFactory: () => {},
isGlobal: true,
extraProviders: ['test' as any],
});
expect(definition.global).to.equal(true);
expect(definition.providers).to.have.length(3);
expect(definition.providers).to.deep.contain('test');
expect(MODULE_OPTIONS_TOKEN).to.equal('RANDOM_TEST_MODULE_OPTIONS');
expect((definition.providers[0] as any).provide).to.equal(
'RANDOM_TEST_MODULE_OPTIONS',
);
try {
expect(ASYNC_OPTIONS_TYPE.imports).to.equal(undefined);
} catch (err) {
expect(err).to.be.instanceOf(Error);
expect(err.message).to.equal(
'"ASYNC_OPTIONS_TYPE" is not supposed to be used as a value.',
);
}
try {
expect(OPTIONS_TYPE.isGlobal).to.equal(undefined);
} catch (err) {
expect(err).to.be.instanceOf(Error);
expect(err.message).to.equal(
'"OPTIONS_TYPE" is not supposed to be used as a value.',
);
}
});
});
});

View File

@@ -1,4 +1,3 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { ArgumentMetadata } from '../../interfaces';
import { ParseIntPipe } from '../../pipes/parse-int.pipe';

View File

@@ -1,9 +1,18 @@
import { expect } from 'chai';
import { HttpStatus } from '../../enums';
import { HttpException } from '../../exceptions';
import { ArgumentMetadata } from '../../interfaces';
import { ParseUUIDPipe } from '../../pipes/parse-uuid.pipe';
class TestException extends HttpException {
constructor() {
super('This is a TestException', HttpStatus.I_AM_A_TEAPOT);
}
}
describe('ParseUUIDPipe', () => {
let target: ParseUUIDPipe;
const exceptionFactory = (error: any) => new TestException();
describe('transform', () => {
const v3 = 'e8b5a51d-11c8-3310-a6ab-367563f20686';
@@ -12,53 +21,80 @@ describe('ParseUUIDPipe', () => {
describe('when validation passes', () => {
it('should return string if value is uuid v3, v4 or v5', async () => {
target = new ParseUUIDPipe();
target = new ParseUUIDPipe({ exceptionFactory });
expect(await target.transform(v3, {} as ArgumentMetadata)).to.equal(v3);
expect(await target.transform(v4, {} as ArgumentMetadata)).to.equal(v4);
expect(await target.transform(v5, {} as ArgumentMetadata)).to.equal(v5);
});
it('should return string if value is uuid v3', async () => {
target = new ParseUUIDPipe({ version: '3' });
target = new ParseUUIDPipe({ version: '3', exceptionFactory });
expect(await target.transform(v3, {} as ArgumentMetadata)).to.equal(v3);
});
it('should return string if value is uuid v4', async () => {
target = new ParseUUIDPipe({ version: '4' });
target = new ParseUUIDPipe({ version: '4', exceptionFactory });
expect(await target.transform(v4, {} as ArgumentMetadata)).to.equal(v4);
});
it('should return string if value is uuid v5', async () => {
target = new ParseUUIDPipe({ version: '5' });
target = new ParseUUIDPipe({ version: '5', exceptionFactory });
expect(await target.transform(v5, {} as ArgumentMetadata)).to.equal(v5);
});
});
describe('when validation fails', () => {
it('should throw an error', async () => {
target = new ParseUUIDPipe();
expect(target.transform('123a', {} as ArgumentMetadata)).to.be.rejected;
target = new ParseUUIDPipe({ exceptionFactory });
await expect(
target.transform('123a', {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
});
it('should throw an error - not a string', async () => {
target = new ParseUUIDPipe({ exceptionFactory });
await expect(
target.transform(undefined, {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
});
it('should throw an error - v3', async () => {
target = new ParseUUIDPipe({ version: '3' });
expect(target.transform('123a', {} as ArgumentMetadata)).to.be.rejected;
expect(target.transform(v4, {} as ArgumentMetadata)).to.be.rejected;
expect(target.transform(v5, {} as ArgumentMetadata)).to.be.rejected;
target = new ParseUUIDPipe({ version: '3', exceptionFactory });
await expect(
target.transform('123a', {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
await expect(
target.transform(v4, {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
await expect(
target.transform(v5, {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
});
it('should throw an error - v4', async () => {
target = new ParseUUIDPipe({ version: '4' });
expect(target.transform('123a', {} as ArgumentMetadata)).to.be.rejected;
expect(target.transform(v3, {} as ArgumentMetadata)).to.be.rejected;
expect(target.transform(v5, {} as ArgumentMetadata)).to.be.rejected;
target = new ParseUUIDPipe({ version: '4', exceptionFactory });
await expect(
target.transform('123a', {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
await expect(
target.transform(v3, {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
await expect(
target.transform(v5, {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
});
it('should throw an error - v5 ', async () => {
target = new ParseUUIDPipe({ version: '5' });
expect(target.transform('123a', {} as ArgumentMetadata)).to.be.rejected;
expect(target.transform(v3, {} as ArgumentMetadata)).to.be.rejected;
expect(target.transform(v4, {} as ArgumentMetadata)).to.be.rejected;
target = new ParseUUIDPipe({ version: '5', exceptionFactory });
await expect(
target.transform('123a', {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
await expect(
target.transform(v3, {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
await expect(
target.transform(v4, {} as ArgumentMetadata),
).to.be.rejectedWith(TestException);
});
});
});

View File

@@ -1,17 +0,0 @@
import { BadRequestException } from '../exceptions';
import { isString } from './shared.utils';
const uuid = {
3: /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i,
4: /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
5: /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
all: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i,
};
export function isUUID(str: any, version = 'all') {
if (!isString(str)) {
throw new BadRequestException('The value passed as UUID is not a string');
}
const pattern = uuid[version];
return pattern && pattern.test(str);
}

View File

@@ -5,9 +5,9 @@ import {
RequestMethod,
MessageEvent,
} from '@nestjs/common';
import { isFunction, isObject } from '@nestjs/common/utils/shared.utils';
import { isObject } from '@nestjs/common/utils/shared.utils';
import { IncomingMessage } from 'http';
import { EMPTY, lastValueFrom, Observable } from 'rxjs';
import { EMPTY, lastValueFrom, Observable, isObservable } from 'rxjs';
import { catchError, debounce, map } from 'rxjs/operators';
import {
AdditionalHeaders,
@@ -64,7 +64,7 @@ export class RouterResponseController {
}
public async transformToResult(resultOrDeferred: any) {
if (resultOrDeferred && isFunction(resultOrDeferred.subscribe)) {
if (isObservable(resultOrDeferred)) {
return lastValueFrom(resultOrDeferred);
}
return resultOrDeferred;
@@ -152,8 +152,8 @@ export class RouterResponseController {
});
}
private assertObservable(result: any) {
if (!isFunction(result.subscribe)) {
private assertObservable(value: any) {
if (!isObservable(value)) {
throw new ReferenceError(
'You must return an Observable stream to use Server-Sent Events (SSE).',
);

View File

@@ -102,13 +102,13 @@ export class DependenciesScanner {
moduleDefinition as Type<any> | DynamicModule,
)
? this.reflectMetadata(
moduleDefinition as Type<any>,
MODULE_METADATA.IMPORTS,
moduleDefinition as Type<any>,
)
: [
...this.reflectMetadata(
(moduleDefinition as DynamicModule).module,
MODULE_METADATA.IMPORTS,
(moduleDefinition as DynamicModule).module,
),
...((moduleDefinition as DynamicModule).imports || []),
];
@@ -177,7 +177,7 @@ export class DependenciesScanner {
context: string,
) {
const modules = [
...this.reflectMetadata(module, MODULE_METADATA.IMPORTS),
...this.reflectMetadata(MODULE_METADATA.IMPORTS, module),
...this.container.getDynamicMetadataByToken(
token,
MODULE_METADATA.IMPORTS as 'imports',
@@ -190,7 +190,7 @@ export class DependenciesScanner {
public reflectProviders(module: Type<any>, token: string) {
const providers = [
...this.reflectMetadata(module, MODULE_METADATA.PROVIDERS),
...this.reflectMetadata(MODULE_METADATA.PROVIDERS, module),
...this.container.getDynamicMetadataByToken(
token,
MODULE_METADATA.PROVIDERS as 'providers',
@@ -204,7 +204,7 @@ export class DependenciesScanner {
public reflectControllers(module: Type<any>, token: string) {
const controllers = [
...this.reflectMetadata(module, MODULE_METADATA.CONTROLLERS),
...this.reflectMetadata(MODULE_METADATA.CONTROLLERS, module),
...this.container.getDynamicMetadataByToken(
token,
MODULE_METADATA.CONTROLLERS as 'controllers',
@@ -229,7 +229,7 @@ export class DependenciesScanner {
public reflectExports(module: Type<unknown>, token: string) {
const exports = [
...this.reflectMetadata(module, MODULE_METADATA.EXPORTS),
...this.reflectMetadata(MODULE_METADATA.EXPORTS, module),
...this.container.getDynamicMetadataByToken(
token,
MODULE_METADATA.EXPORTS as 'exports',
@@ -245,7 +245,7 @@ export class DependenciesScanner {
token: string,
metadataKey: string,
) {
const controllerInjectables = this.reflectMetadata(component, metadataKey);
const controllerInjectables = this.reflectMetadata(metadataKey, component);
const methodsInjectables = this.metadataScanner.scanFromPrototype(
null,
component.prototype,
@@ -416,7 +416,7 @@ export class DependenciesScanner {
this.container.addController(controller, token);
}
public reflectMetadata(metatype: Type<any>, metadataKey: string) {
public reflectMetadata(metadataKey: string, metatype: Type<any>) {
return Reflect.getMetadata(metadataKey, metatype) || [];
}

View File

@@ -71,13 +71,13 @@ describe('RouterResponseController', () => {
describe('transformToResult', () => {
describe('when resultOrDeferred', () => {
describe('is Promise', () => {
it('should return Promise', async () => {
it('should return Promise that resolves to the value resolved by the input Promise', async () => {
const value = 100;
expect(
await routerResponseController.transformToResult(
Promise.resolve(value),
),
).to.be.eq(100);
).to.be.eq(value);
});
});
@@ -88,16 +88,25 @@ describe('RouterResponseController', () => {
await routerResponseController.transformToResult(
of(1, 2, 3, lastValue),
),
).to.be.eq(100);
).to.be.eq(lastValue);
});
});
describe('is value', () => {
it('should return Promise', async () => {
describe('is an object that has the method `subscribe`', () => {
it('should return a Promise that resolves to the input value', async () => {
const value = { subscribe() {} };
expect(
await routerResponseController.transformToResult(value),
).to.equal(value);
});
});
describe('is an ordinary value', () => {
it('should return a Promise that resolves to the input value', async () => {
const value = 100;
expect(
await routerResponseController.transformToResult(value),
).to.be.eq(100);
).to.be.eq(value);
});
});
});

View File

@@ -1,48 +1,31 @@
import { Logger } from '@nestjs/common/services/logger.service';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import {
EmptyError,
fromEvent,
lastValueFrom,
merge,
Subject,
zip,
} from 'rxjs';
import { share, take, tap } from 'rxjs/operators';
import {
CONNECT_EVENT,
ECONNREFUSED,
ERROR_EVENT,
MESSAGE_EVENT,
REDIS_DEFAULT_URL,
REDIS_DEFAULT_HOST,
REDIS_DEFAULT_PORT,
} from '../constants';
import {
ClientOpts,
RedisClient,
RetryStrategyOptions,
} from '../external/redis.interface';
import { ReadPacket, RedisOptions, WritePacket } from '../interfaces';
import { ClientProxy } from './client-proxy';
let redisPackage: any = {};
type Redis = any;
let redisPackage = {} as any;
export class ClientRedis extends ClientProxy {
protected readonly logger = new Logger(ClientProxy.name);
protected readonly subscriptionsCount = new Map<string, number>();
protected readonly url: string;
protected pubClient: RedisClient;
protected subClient: RedisClient;
protected pubClient: Redis;
protected subClient: Redis;
protected connection: Promise<any>;
protected isExplicitlyTerminated = false;
constructor(protected readonly options: RedisOptions['options']) {
super();
this.url =
this.getOptionsProp(options, 'url') ||
(!this.getOptionsProp(options, 'host') && REDIS_DEFAULT_URL);
redisPackage = loadPackage('redis', ClientRedis.name, () =>
require('redis'),
redisPackage = loadPackage('ioredis', ClientRedis.name, () =>
require('ioredis'),
);
this.initializeSerializer(options);
@@ -64,73 +47,57 @@ export class ClientRedis extends ClientProxy {
this.isExplicitlyTerminated = true;
}
public connect(): Promise<any> {
public async connect(): Promise<any> {
if (this.pubClient && this.subClient) {
return this.connection;
}
const error$ = new Subject<Error>();
this.pubClient = this.createClient(error$);
this.subClient = this.createClient(error$);
this.pubClient = this.createClient();
this.subClient = this.createClient();
this.handleError(this.pubClient);
this.handleError(this.subClient);
const pubConnect$ = fromEvent(this.pubClient, CONNECT_EVENT);
const subClient$ = fromEvent(this.subClient, CONNECT_EVENT);
this.connection = Promise.all([
this.subClient.connect(),
this.pubClient.connect(),
]);
await this.connection;
this.connection = lastValueFrom(
merge(error$, zip(pubConnect$, subClient$)).pipe(
take(1),
tap(() =>
this.subClient.on(MESSAGE_EVENT, this.createResponseCallback()),
),
share(),
),
).catch(err => {
if (err instanceof EmptyError) {
return;
}
throw err;
});
this.subClient.on(MESSAGE_EVENT, this.createResponseCallback());
return this.connection;
}
public createClient(error$: Subject<Error>): RedisClient {
return redisPackage.createClient({
...this.getClientOptions(error$),
url: this.url,
public createClient(): Redis {
return new redisPackage({
host: REDIS_DEFAULT_HOST,
port: REDIS_DEFAULT_PORT,
...this.getClientOptions(),
lazyConnect: true,
});
}
public handleError(client: RedisClient) {
public handleError(client: Redis) {
client.addListener(ERROR_EVENT, (err: any) => this.logger.error(err));
}
public getClientOptions(error$: Subject<Error>): Partial<ClientOpts> {
const retry_strategy = (options: RetryStrategyOptions) =>
this.createRetryStrategy(options, error$);
public getClientOptions(): Partial<RedisOptions['options']> {
const retryStrategy = (times: number) => this.createRetryStrategy(times);
return {
...(this.options || {}),
retry_strategy,
retryStrategy,
};
}
public createRetryStrategy(
options: RetryStrategyOptions,
error$: Subject<Error>,
): undefined | number | Error {
if (options.error && (options.error as any).code === ECONNREFUSED) {
error$.error(options.error);
}
public createRetryStrategy(times: number): undefined | number {
if (this.isExplicitlyTerminated) {
return undefined;
}
if (
!this.getOptionsProp(this.options, 'retryAttempts') ||
options.attempt > this.getOptionsProp(this.options, 'retryAttempts')
times > this.getOptionsProp(this.options, 'retryAttempts')
) {
return new Error('Retry time exhausted');
this.logger.error('Retry time exhausted');
return;
}
return this.getOptionsProp(this.options, 'retryDelay') || 0;
}

View File

@@ -2,7 +2,10 @@ import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
export const TCP_DEFAULT_PORT = 3000;
export const TCP_DEFAULT_HOST = 'localhost';
export const REDIS_DEFAULT_URL = 'redis://localhost:6379';
export const REDIS_DEFAULT_PORT = 6379;
export const REDIS_DEFAULT_HOST = 'localhost';
export const NATS_DEFAULT_URL = 'nats://localhost:4222';
export const MQTT_DEFAULT_URL = 'mqtt://localhost:1883';
export const GRPC_DEFAULT_URL = 'localhost:5000';
@@ -38,8 +41,10 @@ export const RQM_DEFAULT_NOACK = true;
export const RQM_DEFAULT_PERSISTENT = false;
export const GRPC_DEFAULT_PROTO_LOADER = '@grpc/proto-loader';
export const NO_EVENT_HANDLER = (text: TemplateStringsArray, pattern: string) =>
`There is no matching event handler defined in the remote service. Event pattern: ${pattern}`;
export const NO_MESSAGE_HANDLER = `There is no matching message handler defined in the remote service.`;
export const NO_EVENT_HANDLER = `There is no matching event handler defined in the remote service.`;
export const DISCONNECTED_RMQ_MESSAGE = `Disconnected from RMQ. Trying to reconnect.`;
export const KAFKA_DEFAULT_CLIENT = 'nestjs-consumer';

View File

@@ -1,5 +1,5 @@
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { Observable, isObservable } from 'rxjs';
import { isObservable, Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { RpcExceptionsHandler } from '../exceptions/rpc-exceptions-handler';

View File

@@ -1,7 +1,12 @@
import { KafkaMessage } from '../external/kafka.interface';
import { Consumer, KafkaMessage } from '../external/kafka.interface';
import { BaseRpcContext } from './base-rpc.context';
type KafkaContextArgs = [KafkaMessage, number, string];
type KafkaContextArgs = [
message: KafkaMessage,
partition: number,
topic: string,
consumer: Consumer,
];
export class KafkaContext extends BaseRpcContext<KafkaContextArgs> {
constructor(args: KafkaContextArgs) {
@@ -28,4 +33,11 @@ export class KafkaContext extends BaseRpcContext<KafkaContextArgs> {
getTopic() {
return this.args[2];
}
/**
* Returns the Kafka consumer reference.
*/
getConsumer() {
return this.args[3];
}
}

View File

@@ -48,7 +48,11 @@ export const EventPattern: {
key: string | symbol,
descriptor: PropertyDescriptor,
) => {
Reflect.defineMetadata(PATTERN_METADATA, metadata, descriptor.value);
Reflect.defineMetadata(
PATTERN_METADATA,
[].concat(metadata),
descriptor.value,
);
Reflect.defineMetadata(
PATTERN_HANDLER_METADATA,
PatternHandler.EVENT,

View File

@@ -62,7 +62,11 @@ export const MessagePattern: {
key: string | symbol,
descriptor: PropertyDescriptor,
) => {
Reflect.defineMetadata(PATTERN_METADATA, metadata, descriptor.value);
Reflect.defineMetadata(
PATTERN_METADATA,
[].concat(metadata),
descriptor.value,
);
Reflect.defineMetadata(
PATTERN_HANDLER_METADATA,
PatternHandler.MESSAGE,

View File

@@ -0,0 +1,21 @@
import { IncomingEvent, IncomingRequest } from '../interfaces';
import { KafkaRequest } from '../serializers/kafka-request.serializer';
import { IncomingRequestDeserializer } from './incoming-request.deserializer';
export class KafkaRequestDeserializer extends IncomingRequestDeserializer {
mapToSchema(
data: KafkaRequest,
options?: Record<string, any>,
): IncomingRequest | IncomingEvent {
if (!options) {
return {
pattern: undefined,
data: undefined,
};
}
return {
pattern: options.channel,
data: data?.value ?? data,
};
}
}

View File

@@ -1,2 +1,3 @@
export * from './base-rpc-exception-filter';
export * from './kafka-retriable-exception';
export * from './rpc-exception';

View File

@@ -0,0 +1,17 @@
import { RpcException } from './rpc-exception';
/**
* Exception that instructs Kafka driver to instead of introspecting
* error processing flow and sending serialized error message to the consumer,
* force bubble it up to the "eachMessage" callback of the underlying "kafkajs" package
* (even if interceptors are applied, or an observable stream is returned from the message handler).
*
* A transient exception that if retried may succeed.
*
* @publicApi
*/
export class KafkaRetriableException extends RpcException {
public getError(): string | object {
return this;
}
}

View File

@@ -16,5 +16,5 @@ export interface ChannelOptions {
'grpc.max_reconnect_backoff_ms'?: number;
'grpc.use_local_subchannel_pool'?: number;
'grpc-node.max_session_memory'?: number;
[key: string]: string | number | undefined;
[key: string]: any;
}

View File

@@ -1,383 +1,213 @@
/* eslint-disable @typescript-eslint/adjacent-overload-signatures */
import { ConnectionOptions } from 'tls';
export interface RetryStrategyOptions {
error: Error;
total_retry_time: number;
times_connected: number;
attempt: number;
}
/**
* @see https://github.dev/luin/ioredis/blob/df04dd8d87a44d3b64b385c86581915248554508/lib/redis/RedisOptions.ts#L184
*/
export interface IORedisOptions {
Connector?: any;
retryStrategy?: (times: number) => number | void | null;
export interface ClientOpts {
auth_pass?: string;
command_queue_high_water?: number;
command_queue_low_water?: number;
connect_timeout?: number;
db?: string;
detect_buffers?: boolean;
disable_resubscribing?: boolean;
enable_offline_queue?: boolean;
family?: string;
host?: string;
max_attempts?: number;
no_ready_check?: boolean;
parser?: string;
/**
* If a command does not return a reply within a set number of milliseconds,
* a "Command timed out" error will be thrown.
*/
commandTimeout?: number;
/**
* Enable/disable keep-alive functionality.
* @link https://nodejs.org/api/net.html#socketsetkeepaliveenable-initialdelay
* @default 0
*/
keepAlive?: number;
/**
* Enable/disable the use of Nagle's algorithm.
* @link https://nodejs.org/api/net.html#socketsetnodelaynodelay
* @default true
*/
noDelay?: boolean;
/**
* Set the name of the connection to make it easier to identity the connection
* in client list.
* @link https://redis.io/commands/client-setname
*/
connectionName?: string;
/**
* If set, client will send AUTH command with the value of this option as the first argument when connected.
* This is supported since Redis 6.
*/
username?: string;
/**
* If set, client will send AUTH command with the value of this option when connected.
*/
password?: string;
path?: string;
port?: number;
prefix?: string;
rename_commands?: any;
retry_max_delay?: number;
retry_strategy?: any;
retry_unfulfilled_commands?: boolean;
return_buffers?: boolean;
socket_keepalive?: boolean;
socket_nodelay?: boolean;
string_numbers?: boolean;
tls?: any;
url?: string;
}
export interface RedisClient {
// event: connect
// event: error
// event: message
// event: pmessage
// event: subscribe
// event: psubscribe
// event: unsubscribe
// event: punsubscribe
connected: boolean;
retry_delay: number;
retry_backoff: number;
command_queue: any[];
offline_queue: any[];
server_info: any;
/**
* Forcibly close the connection to the Redis server. Note that this does not wait until all replies have been parsed. If you want to exit cleanly, call client.quit()
* Database index to use.
*
* @param {boolean} flush You should set flush to true, if you are not absolutely sure you do not care about any other commands. If you set flush to false all still running commands will silently fail.
* @default 0
*/
end(flush: boolean): void;
unref(): void;
db?: number;
/**
* Stop sending commands and queue the commands.
* When the client reconnects, channels subscribed in the previous connection will be
* resubscribed automatically if `autoResubscribe` is `true`.
* @default true
*/
cork(): void;
autoResubscribe?: boolean;
/**
* Resume and send the queued commands at once.
* Whether or not to resend unfulfilled commands on reconnect.
* Unfulfilled commands are most likely to be blocking commands such as `brpop` or `blpop`.
* @default true
*/
uncork(): void;
autoResendUnfulfilledCommands?: boolean;
/**
* Whether or not to reconnect on certain Redis errors.
* This options by default is `null`, which means it should never reconnect on Redis errors.
* You can pass a function that accepts an Redis error, and returns:
* - `true` or `1` to trigger a reconnection.
* - `false` or `0` to not reconnect.
* - `2` to reconnect and resend the failed command (who triggered the error) after reconnection.
* @example
* ```js
* const redis = new Redis({
* reconnectOnError(err) {
* const targetError = "READONLY";
* if (err.message.includes(targetError)) {
* // Only reconnect when the error contains "READONLY"
* return true; // or `return 1;`
* }
* },
* });
* ```
* @default null
*/
reconnectOnError?: ((err: Error) => boolean | 1 | 2) | null;
// Low level command execution
send_command(command: string, ...args: any[]): boolean;
/**
* @default false
*/
readOnly?: boolean;
/**
* When enabled, numbers returned by Redis will be converted to JavaScript strings instead of numbers.
* This is necessary if you want to handle big numbers (above `Number.MAX_SAFE_INTEGER` === 2^53).
* @default false
*/
stringNumbers?: boolean;
// Connection (http://redis.io/commands#connection)
auth(password: string, callback?: any): boolean;
ping(callback?: any): boolean;
/**
* How long the client will wait before killing a socket due to inactivity during initial connection.
* @default 10000
*/
connectTimeout?: number;
// Strings (http://redis.io/commands#strings)
append(key: string, value: string, callback?: any): boolean;
bitcount(key: string, callback?: any): boolean;
bitcount(key: string, start: number, end: number, callback?: any): boolean;
set(key: string, value: string, callback?: any): boolean;
get(key: string, callback?: any): boolean;
exists(key: string, value: string, callback?: any): boolean;
/**
* This option is used internally when you call `redis.monitor()` to tell Redis
* to enter the monitor mode when the connection is established.
*
* @default false
*/
monitor?: boolean;
publish(channel: string, value: any): boolean;
subscribe(channel: string): boolean;
on(event: string, callback: Function): any;
off(event: string, callback: Function): any;
addListener(event: string, callback: Function): any;
/**
* The commands that don't get a reply due to the connection to the server is lost are
* put into a queue and will be resent on reconnect (if allowed by the `retryStrategy` option).
* This option is used to configure how many reconnection attempts should be allowed before
* the queue is flushed with a `MaxRetriesPerRequestError` error.
* Set this options to `null` instead of a number to let commands wait forever
* until the connection is alive again.
*
* @default 20
*/
maxRetriesPerRequest?: number | null;
/*
commands = set_union([
"get", "set", "setnx", "setex", "append", "strlen", "del", "exists", "setbit", "getbit", "setrange", "getrange", "substr",
"incr", "decr", "mget", "rpush", "lpush", "rpushx", "lpushx", "linsert", "rpop", "lpop", "brpop", "brpoplpush", "blpop", "llen", "lindex",
"lset", "lrange", "ltrim", "lrem", "rpoplpush", "sadd", "srem", "smove", "sismember", "scard", "spop", "srandmember", "sinter", "sinterstore",
"sunion", "sunionstore", "sdiff", "sdiffstore", "smembers", "zadd", "zincrby", "zrem", "zremrangebyscore", "zremrangebyrank", "zunionstore",
"zinterstore", "zrange", "zrangebyscore", "zrevrangebyscore", "zcount", "zrevrange", "zcard", "zscore", "zrank", "zrevrank", "hset", "hsetnx",
"hget", "hmset", "hmget", "hincrby", "hincrbyfloat", "hdel", "hlen", "hkeys", "hvals", "hgetall", "hexists", "incrby", "decrby", "getset", "mset", "msetnx",
"randomkey", "select", "move", "rename", "renamenx", "expire", "expireat", "keys", "dbsize", "auth", "ping", "echo", "save", "bgsave",
"bgrewriteaof", "shutdown", "lastsave", "type", "any", "exec", "discard", "sync", "flushdb", "flushall", "sort", "info", "monitor", "ttl",
"persist", "slaveof", "debug", "config", "subscribe", "unsubscribe", "psubscribe", "punsubscribe", "publish", "watch", "unwatch", "cluster",
"restore", "migrate", "dump", "object", "client", "eval", "evalsha"], require("./lib/commands"));
/**
* @default 10000
*/
maxLoadingRetryTime?: number;
/**
* @default false
*/
enableAutoPipelining?: boolean;
/**
* @default []
*/
autoPipeliningIgnoredCommands?: string[];
offlineQueue?: boolean;
commandQueue?: boolean;
/**
*
* By default, if the connection to Redis server has not been established, commands are added to a queue
* and are executed once the connection is "ready" (when `enableReadyCheck` is true, "ready" means
* the Redis server has loaded the database from disk, otherwise means the connection to the Redis
* server has been established). If this option is false, when execute the command when the connection
* isn't ready, an error will be returned.
*
* @default true
*/
enableOfflineQueue?: boolean;
/**
* The client will sent an INFO command to check whether the server is still loading data from the disk (
* which happens when the server is just launched) when the connection is established, and only wait until
* the loading process is finished before emitting the `ready` event.
*
* @default true
*/
enableReadyCheck?: boolean;
/**
* When a Redis instance is initialized, a connection to the server is immediately established. Set this to
* true will delay the connection to the server until the first command is sent or `redis.connect()` is called
* explicitly.
*
* @default false
*/
get(args: any[], callback?: any): boolean;
get(...args: any[]): boolean;
set(args: any[], callback?: any): boolean;
set(...args: any[]): boolean;
setnx(args: any[], callback?: any): boolean;
setnx(...args: any[]): boolean;
setex(args: any[], callback?: any): boolean;
setex(...args: any[]): boolean;
append(args: any[], callback?: any): boolean;
append(...args: any[]): boolean;
strlen(args: any[], callback?: any): boolean;
strlen(...args: any[]): boolean;
del(args: any[], callback?: any): boolean;
del(...args: any[]): boolean;
exists(args: any[], callback?: any): boolean;
exists(...args: any[]): boolean;
setbit(args: any[], callback?: any): boolean;
setbit(...args: any[]): boolean;
getbit(args: any[], callback?: any): boolean;
getbit(...args: any[]): boolean;
setrange(args: any[], callback?: any): boolean;
setrange(...args: any[]): boolean;
getrange(args: any[], callback?: any): boolean;
getrange(...args: any[]): boolean;
substr(args: any[], callback?: any): boolean;
substr(...args: any[]): boolean;
incr(args: any[], callback?: any): boolean;
incr(...args: any[]): boolean;
decr(args: any[], callback?: any): boolean;
decr(...args: any[]): boolean;
mget(args: any[], callback?: any): boolean;
mget(...args: any[]): boolean;
rpush(...args: any[]): boolean;
lpush(args: any[], callback?: any): boolean;
lpush(...args: any[]): boolean;
rpushx(args: any[], callback?: any): boolean;
rpushx(...args: any[]): boolean;
lpushx(args: any[], callback?: any): boolean;
lpushx(...args: any[]): boolean;
linsert(args: any[], callback?: any): boolean;
linsert(...args: any[]): boolean;
rpop(args: any[], callback?: any): boolean;
rpop(...args: any[]): boolean;
lpop(args: any[], callback?: any): boolean;
lpop(...args: any[]): boolean;
brpop(args: any[], callback?: any): boolean;
brpop(...args: any[]): boolean;
brpoplpush(args: any[], callback?: any): boolean;
brpoplpush(...args: any[]): boolean;
blpop(args: any[], callback?: any): boolean;
blpop(...args: any[]): boolean;
llen(args: any[], callback?: any): boolean;
llen(...args: any[]): boolean;
lindex(args: any[], callback?: any): boolean;
lindex(...args: any[]): boolean;
lset(args: any[], callback?: any): boolean;
lset(...args: any[]): boolean;
lrange(args: any[], callback?: any): boolean;
lrange(...args: any[]): boolean;
ltrim(args: any[], callback?: any): boolean;
ltrim(...args: any[]): boolean;
lrem(args: any[], callback?: any): boolean;
lrem(...args: any[]): boolean;
rpoplpush(args: any[], callback?: any): boolean;
rpoplpush(...args: any[]): boolean;
sadd(args: any[], callback?: any): boolean;
sadd(...args: any[]): boolean;
srem(args: any[], callback?: any): boolean;
srem(...args: any[]): boolean;
smove(args: any[], callback?: any): boolean;
smove(...args: any[]): boolean;
sismember(args: any[], callback?: any): boolean;
sismember(...args: any[]): boolean;
scard(args: any[], callback?: any): boolean;
scard(...args: any[]): boolean;
spop(args: any[], callback?: any): boolean;
spop(...args: any[]): boolean;
srandmember(args: any[], callback?: any): boolean;
srandmember(...args: any[]): boolean;
sinter(args: any[], callback?: any): boolean;
sinter(...args: any[]): boolean;
sinterstore(args: any[], callback?: any): boolean;
sinterstore(...args: any[]): boolean;
sunion(args: any[], callback?: any): boolean;
sunion(...args: any[]): boolean;
sunionstore(args: any[], callback?: any): boolean;
sunionstore(...args: any[]): boolean;
sdiff(args: any[], callback?: any): boolean;
sdiff(...args: any[]): boolean;
sdiffstore(args: any[], callback?: any): boolean;
sdiffstore(...args: any[]): boolean;
smembers(args: any[], callback?: any): boolean;
smembers(...args: any[]): boolean;
zadd(args: any[], callback?: any): boolean;
zadd(...args: any[]): boolean;
zincrby(args: any[], callback?: any): boolean;
zincrby(...args: any[]): boolean;
zrem(args: any[], callback?: any): boolean;
zrem(...args: any[]): boolean;
zremrangebyscore(args: any[], callback?: any): boolean;
zremrangebyscore(...args: any[]): boolean;
zremrangebyrank(args: any[], callback?: any): boolean;
zremrangebyrank(...args: any[]): boolean;
zunionstore(args: any[], callback?: any): boolean;
zunionstore(...args: any[]): boolean;
zinterstore(args: any[], callback?: any): boolean;
zinterstore(...args: any[]): boolean;
zrange(args: any[], callback?: any): boolean;
zrange(...args: any[]): boolean;
zrangebyscore(args: any[], callback?: any): boolean;
zrangebyscore(...args: any[]): boolean;
zrevrangebyscore(args: any[], callback?: any): boolean;
zrevrangebyscore(...args: any[]): boolean;
zcount(args: any[], callback?: any): boolean;
zcount(...args: any[]): boolean;
zrevrange(args: any[], callback?: any): boolean;
zrevrange(...args: any[]): boolean;
zcard(args: any[], callback?: any): boolean;
zcard(...args: any[]): boolean;
zscore(args: any[], callback?: any): boolean;
zscore(...args: any[]): boolean;
zrank(args: any[], callback?: any): boolean;
zrank(...args: any[]): boolean;
zrevrank(args: any[], callback?: any): boolean;
zrevrank(...args: any[]): boolean;
hset(args: any[], callback?: any): boolean;
hset(...args: any[]): boolean;
hsetnx(args: any[], callback?: any): boolean;
hsetnx(...args: any[]): boolean;
hget(args: any[], callback?: any): boolean;
hget(...args: any[]): boolean;
hmset(args: any[], callback?: any): boolean;
hmset(key: string, hash: any, callback?: any): boolean;
hmset(...args: any[]): boolean;
hmget(args: any[], callback?: any): boolean;
hmget(...args: any[]): boolean;
hincrby(args: any[], callback?: any): boolean;
hincrby(...args: any[]): boolean;
hincrbyfloat(args: any[], callback?: any): boolean;
hincrbyfloat(...args: any[]): boolean;
hdel(args: any[], callback?: any): boolean;
hdel(...args: any[]): boolean;
hlen(args: any[], callback?: any): boolean;
hlen(...args: any[]): boolean;
hkeys(args: any[], callback?: any): boolean;
hkeys(...args: any[]): boolean;
hvals(args: any[], callback?: any): boolean;
hvals(...args: any[]): boolean;
hgetall(args: any[], callback?: any): boolean;
hgetall(...args: any[]): boolean;
hgetall(key: string, callback?: any): boolean;
hexists(args: any[], callback?: any): boolean;
hexists(...args: any[]): boolean;
incrby(args: any[], callback?: any): boolean;
incrby(...args: any[]): boolean;
decrby(args: any[], callback?: any): boolean;
decrby(...args: any[]): boolean;
getset(args: any[], callback?: any): boolean;
getset(...args: any[]): boolean;
mset(args: any[], callback?: any): boolean;
mset(...args: any[]): boolean;
msetnx(args: any[], callback?: any): boolean;
msetnx(...args: any[]): boolean;
randomkey(args: any[], callback?: any): boolean;
randomkey(...args: any[]): boolean;
select(args: any[], callback?: any): void;
select(...args: any[]): void;
move(args: any[], callback?: any): boolean;
move(...args: any[]): boolean;
rename(args: any[], callback?: any): boolean;
rename(...args: any[]): boolean;
renamenx(args: any[], callback?: any): boolean;
renamenx(...args: any[]): boolean;
expire(args: any[], callback?: any): boolean;
expire(...args: any[]): boolean;
expireat(args: any[], callback?: any): boolean;
expireat(...args: any[]): boolean;
keys(args: any[], callback?: any): boolean;
keys(...args: any[]): boolean;
dbsize(args: any[], callback?: any): boolean;
dbsize(...args: any[]): boolean;
auth(args: any[], callback?: any): void;
auth(...args: any[]): void;
ping(args: any[], callback?: any): boolean;
ping(...args: any[]): boolean;
echo(args: any[], callback?: any): boolean;
echo(...args: any[]): boolean;
save(args: any[], callback?: any): boolean;
save(...args: any[]): boolean;
bgsave(args: any[], callback?: any): boolean;
bgsave(...args: any[]): boolean;
bgrewriteaof(args: any[], callback?: any): boolean;
bgrewriteaof(...args: any[]): boolean;
shutdown(args: any[], callback?: any): boolean;
shutdown(...args: any[]): boolean;
lastsave(args: any[], callback?: any): boolean;
lastsave(...args: any[]): boolean;
type(args: any[], callback?: any): boolean;
type(...args: any[]): boolean;
any(args: any[], callback?: any): any;
any(...args: any[]): any;
exec(args: any[], callback?: any): boolean;
exec(...args: any[]): boolean;
discard(args: any[], callback?: any): boolean;
discard(...args: any[]): boolean;
sync(args: any[], callback?: any): boolean;
sync(...args: any[]): boolean;
flushdb(args: any[], callback?: any): boolean;
flushdb(...args: any[]): boolean;
flushall(args: any[], callback?: any): boolean;
flushall(...args: any[]): boolean;
sort(args: any[], callback?: any): boolean;
sort(...args: any[]): boolean;
info(args: any[], callback?: any): boolean;
info(...args: any[]): boolean;
monitor(args: any[], callback?: any): boolean;
monitor(...args: any[]): boolean;
ttl(args: any[], callback?: any): boolean;
ttl(...args: any[]): boolean;
persist(args: any[], callback?: any): boolean;
persist(...args: any[]): boolean;
slaveof(args: any[], callback?: any): boolean;
slaveof(...args: any[]): boolean;
debug(args: any[], callback?: any): boolean;
debug(...args: any[]): boolean;
config(args: any[], callback?: any): boolean;
config(...args: any[]): boolean;
subscribe(args: any[], callback?: any): boolean;
subscribe(...args: any[]): boolean;
unsubscribe(args: any[], callback?: any): boolean;
unsubscribe(...args: any[]): boolean;
psubscribe(args: any[], callback?: any): boolean;
psubscribe(...args: any[]): boolean;
punsubscribe(args: any[], callback?: any): boolean;
punsubscribe(...args: any[]): boolean;
publish(args: any[], callback?: any): boolean;
publish(...args: any[]): boolean;
watch(args: any[], callback?: any): boolean;
watch(...args: any[]): boolean;
unwatch(args: any[], callback?: any): boolean;
unwatch(...args: any[]): boolean;
cluster(args: any[], callback?: any): boolean;
cluster(...args: any[]): boolean;
restore(args: any[], callback?: any): boolean;
restore(...args: any[]): boolean;
migrate(args: any[], callback?: any): boolean;
migrate(...args: any[]): boolean;
dump(args: any[], callback?: any): boolean;
dump(...args: any[]): boolean;
object(args: any[], callback?: any): boolean;
object(...args: any[]): boolean;
client(args: any[], callback?: any): boolean;
client(...args: any[]): boolean;
eval(args: any[], callback?: any): boolean;
eval(...args: any[]): boolean;
evalsha(args: any[], callback?: any): boolean;
evalsha(...args: any[]): boolean;
script(args: any[], callback?: any): boolean;
script(...args: any[]): boolean;
script(key: string, callback?: any): boolean;
quit(args: any[], callback?: any): boolean;
quit(...args: any[]): boolean;
sscan(...args: any[]): boolean;
sscan(args: any[], callback?: any): boolean;
scan(...args: any[]): boolean;
scan(args: any[], callback?: any): boolean;
hscan(...args: any[]): boolean;
hscan(args: any[], callback?: any): boolean;
zscan(...args: any[]): boolean;
zscan(args: any[], callback?: any): boolean;
lazyConnect?: boolean;
// Extras
duplicate(options?: any[], callback?: any): RedisClient;
/**
* @default undefined
*/
scripts?: Record<
string,
{ lua: string; numberOfKeys?: number; readOnly?: boolean }
>;
keyPrefix?: string;
showFriendlyErrorStack?: boolean;
// StandaloneConnectionOptions
disconnectTimeout?: number;
tls?: ConnectionOptions;
// SentinelConnectionOptions
/**
* Master group name of the Sentinel
*/
name?: string;
/**
* @default "master"
*/
role?: 'master' | 'slave';
sentinelUsername?: string;
sentinelPassword?: string;
sentinels?: Array<Partial<any>>;
sentinelRetryStrategy?: (retryAttempts: number) => number | void | null;
sentinelReconnectStrategy?: (retryAttempts: number) => number | void | null;
preferredSlaves?: any;
sentinelCommandTimeout?: number;
enableTLSForSentinelMode?: boolean;
sentinelTLS?: ConnectionOptions;
natMap?: any;
updateSentinels?: boolean;
/**
* @default 10
*/
sentinelMaxConnections?: number;
failoverDetector?: boolean;
}

View File

@@ -10,7 +10,7 @@ import {
ProducerRecord,
} from '../external/kafka.interface';
import { MqttClientOptions, QoS } from '../external/mqtt-options.interface';
import { ClientOpts } from '../external/redis.interface';
import { IORedisOptions } from '../external/redis.interface';
import { RmqUrl } from '../external/rmq-url.interface';
import { TcpSocket } from '../helpers';
import { CustomTransportStrategy } from './custom-transport-strategy.interface';
@@ -35,9 +35,6 @@ export interface CustomStrategy {
export interface GrpcOptions {
transport?: Transport.GRPC;
options: {
interceptors?: Array<
(options: any, nextCall: (options: any) => any) => any
>;
url?: string;
maxSendMessageLength?: number;
maxReceiveMessageLength?: number;
@@ -94,7 +91,7 @@ export interface RedisOptions {
retryDelay?: number;
serializer?: Serializer;
deserializer?: Deserializer;
} & ClientOpts;
} & IORedisOptions;
}
export interface MqttOptions {

View File

@@ -20,7 +20,7 @@ export interface ClientProperties {
}
export interface EventOrMessageListenerDefinition {
pattern: PatternMetadata;
patterns: PatternMetadata[];
methodKey: string;
isEventHandler: boolean;
targetCallback: (...args: any[]) => any;
@@ -58,13 +58,13 @@ export class ListenerMetadataExplorer {
if (isUndefined(handlerType)) {
return;
}
const pattern = Reflect.getMetadata(PATTERN_METADATA, targetCallback);
const patterns = Reflect.getMetadata(PATTERN_METADATA, targetCallback);
const transport = Reflect.getMetadata(TRANSPORT_METADATA, targetCallback);
const extras = Reflect.getMetadata(PATTERN_EXTRAS_METADATA, targetCallback);
return {
methodKey,
targetCallback,
pattern,
patterns,
transport,
extras,
isEventHandler: handlerType === PatternHandler.EVENT,

View File

@@ -71,8 +71,21 @@ export class ListenersController {
isUndefined(server.transportId) ||
transport === server.transportId,
)
.reduce((acc, handler) => {
// Optional chaining for backward-compatibility
handler.patterns?.forEach(pattern =>
acc.push({ ...handler, patterns: [pattern] }),
);
return acc;
}, [])
.forEach(
({ pattern, targetCallback, methodKey, extras, isEventHandler }) => {
({
patterns: [pattern],
targetCallback,
methodKey,
extras,
isEventHandler,
}) => {
if (isStatic) {
const proxy = this.contextCreator.create(
instance as object,

View File

@@ -24,13 +24,12 @@ export class MicroservicesModule {
private listenersController: ListenersController;
public register(container: NestContainer, config: ApplicationConfig) {
const rpcProxy = new RpcProxy();
const exceptionFiltersContext = new ExceptionFiltersContext(
container,
config,
);
const contextCreator = new RpcContextCreator(
rpcProxy,
new RpcProxy(),
exceptionFiltersContext,
new PipesContextCreator(container, config),
new PipesConsumer(),

View File

@@ -1,14 +1,17 @@
import { Logger } from '@nestjs/common/services/logger.service';
import { isNil } from '@nestjs/common/utils/shared.utils';
import { Observable } from 'rxjs';
import { isObservable, lastValueFrom, Observable, ReplaySubject } from 'rxjs';
import {
KAFKA_DEFAULT_BROKER,
KAFKA_DEFAULT_CLIENT,
KAFKA_DEFAULT_GROUP,
NO_EVENT_HANDLER,
NO_MESSAGE_HANDLER,
} from '../constants';
import { KafkaContext } from '../ctx-host';
import { KafkaRequestDeserializer } from '../deserializers/kafka-request.deserializer';
import { KafkaHeaders, Transport } from '../enums';
import { KafkaRetriableException } from '../exceptions';
import {
BrokersFunction,
Consumer,
@@ -26,6 +29,7 @@ import {
CustomTransportStrategy,
KafkaOptions,
OutgoingResponse,
ReadPacket,
} from '../interfaces';
import { KafkaRequestSerializer } from '../serializers/kafka-request.serializer';
import { Server } from './server';
@@ -162,6 +166,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
rawMessage,
payload.partition,
payload.topic,
this.consumer,
]);
const handler = this.getHandlerByPattern(packet.pattern);
// if the correlation id or reply topic is not set
@@ -186,7 +191,37 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
const response$ = this.transformToObservable(
await handler(packet.data, kafkaContext),
);
response$ && this.send(response$, publish);
const replayStream$ = new ReplaySubject();
await this.combineStreamsAndThrowIfRetriable(response$, replayStream$);
this.send(replayStream$, publish);
}
private combineStreamsAndThrowIfRetriable(
response$: Observable<any>,
replayStream$: ReplaySubject<unknown>,
) {
return new Promise<void>((resolve, reject) => {
let isPromiseResolved = false;
response$.subscribe({
next: val => {
replayStream$.next(val);
if (!isPromiseResolved) {
isPromiseResolved = true;
resolve();
}
},
error: err => {
if (err instanceof KafkaRetriableException && !isPromiseResolved) {
isPromiseResolved = true;
reject(err);
}
replayStream$.error(err);
},
complete: () => replayStream$.complete(),
});
});
}
public async sendMessage(
@@ -228,9 +263,12 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
if (!outgoingResponse.err) {
return;
}
outgoingMessage.headers[KafkaHeaders.NEST_ERR] = Buffer.from(
outgoingResponse.err,
);
const stringifiedError =
typeof outgoingResponse.err === 'object'
? JSON.stringify(outgoingResponse.err)
: outgoingResponse.err;
outgoingMessage.headers[KafkaHeaders.NEST_ERR] =
Buffer.from(stringifiedError);
}
public assignCorrelationIdHeader(
@@ -251,8 +289,27 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
outgoingMessage.partition = parseFloat(replyPartition);
}
public async handleEvent(
pattern: string,
packet: ReadPacket,
context: KafkaContext,
): Promise<any> {
const handler = this.getHandlerByPattern(pattern);
if (!handler) {
return this.logger.error(NO_EVENT_HANDLER`${pattern}`);
}
const resultOrStream = await handler(packet.data, context);
if (isObservable(resultOrStream)) {
await lastValueFrom(resultOrStream);
}
}
protected initializeSerializer(options: KafkaOptions['options']) {
this.serializer =
(options && options.serializer) || new KafkaRequestSerializer();
}
protected initializeDeserializer(options: KafkaOptions['options']) {
this.deserializer = options?.deserializer ?? new KafkaRequestDeserializer();
}
}

View File

@@ -1,41 +1,37 @@
import { isUndefined } from '@nestjs/common/utils/shared.utils';
import { Observable } from 'rxjs';
import {
CONNECT_EVENT,
ERROR_EVENT,
MESSAGE_EVENT,
NO_MESSAGE_HANDLER,
REDIS_DEFAULT_URL,
REDIS_DEFAULT_HOST,
REDIS_DEFAULT_PORT,
} from '../constants';
import { RedisContext } from '../ctx-host';
import { Transport } from '../enums';
import {
ClientOpts,
RedisClient,
RetryStrategyOptions,
} from '../external/redis.interface';
import { CustomTransportStrategy, IncomingRequest } from '../interfaces';
import { RedisOptions } from '../interfaces/microservice-configuration.interface';
CustomTransportStrategy,
IncomingRequest,
RedisOptions,
} from '../interfaces';
import { Server } from './server';
let redisPackage: any = {};
type Redis = any;
let redisPackage = {} as any;
export class ServerRedis extends Server implements CustomTransportStrategy {
public readonly transportId = Transport.REDIS;
private readonly url: string;
private subClient: RedisClient;
private pubClient: RedisClient;
private subClient: Redis;
private pubClient: Redis;
private isExplicitlyTerminated = false;
constructor(private readonly options: RedisOptions['options']) {
super();
this.url =
this.getOptionsProp(options, 'url') ||
(!this.getOptionsProp(options, 'host') && REDIS_DEFAULT_URL);
redisPackage = this.loadPackage('redis', ServerRedis.name, () =>
require('redis'),
redisPackage = this.loadPackage('ioredis', ServerRedis.name, () =>
require('ioredis'),
);
this.initializeSerializer(options);
@@ -51,6 +47,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
this.handleError(this.pubClient);
this.handleError(this.subClient);
this.start(callback);
} catch (err) {
callback(err);
@@ -58,11 +55,15 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
}
public start(callback?: () => void) {
this.bindEvents(this.subClient, this.pubClient);
this.subClient.on(CONNECT_EVENT, callback);
Promise.all([this.subClient.connect(), this.pubClient.connect()])
.then(() => {
this.bindEvents(this.subClient, this.pubClient);
callback();
})
.catch(callback);
}
public bindEvents(subClient: RedisClient, pubClient: RedisClient) {
public bindEvents(subClient: Redis, pubClient: Redis) {
subClient.on(MESSAGE_EVENT, this.getMessageHandler(pubClient).bind(this));
const subscribePatterns = [...this.messageHandlers.keys()];
subscribePatterns.forEach(pattern => {
@@ -79,14 +80,16 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
this.subClient && this.subClient.quit();
}
public createRedisClient(): RedisClient {
return redisPackage.createClient({
public createRedisClient(): Redis {
return new redisPackage({
port: REDIS_DEFAULT_PORT,
host: REDIS_DEFAULT_HOST,
...this.getClientOptions(),
url: this.url,
lazyConnect: true,
});
}
public getMessageHandler(pub: RedisClient) {
public getMessageHandler(pub: Redis) {
return async (channel: string, buffer: string | any) =>
this.handleMessage(channel, buffer, pub);
}
@@ -94,7 +97,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
public async handleMessage(
channel: string,
buffer: string | any,
pub: RedisClient,
pub: Redis,
) {
const rawMessage = this.parseMessage(buffer);
const packet = await this.deserializer.deserialize(rawMessage, { channel });
@@ -125,7 +128,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
response$ && this.send(response$, publish);
}
public getPublisher(pub: RedisClient, pattern: any, id: string) {
public getPublisher(pub: Redis, pattern: any, id: string) {
return (response: any) => {
Object.assign(response, { id });
const outgoingResponse = this.serializer.serialize(response);
@@ -157,31 +160,25 @@ export class ServerRedis extends Server implements CustomTransportStrategy {
stream.on(ERROR_EVENT, (err: any) => this.logger.error(err));
}
public getClientOptions(): Partial<ClientOpts> {
const retry_strategy = (options: RetryStrategyOptions) =>
this.createRetryStrategy(options);
public getClientOptions(): Partial<RedisOptions['options']> {
const retryStrategy = (times: number) => this.createRetryStrategy(times);
return {
...(this.options || {}),
retry_strategy,
retryStrategy,
};
}
public createRetryStrategy(
options: RetryStrategyOptions,
): undefined | number | void {
if (options.error && (options.error as any).code === 'ECONNREFUSED') {
this.logger.error(`Error ECONNREFUSED: ${this.url}`);
}
public createRetryStrategy(times: number): undefined | number | void {
if (this.isExplicitlyTerminated) {
return undefined;
}
if (
!this.getOptionsProp(this.options, 'retryAttempts') ||
options.attempt > this.getOptionsProp(this.options, 'retryAttempts')
times > this.getOptionsProp(this.options, 'retryAttempts')
) {
this.logger.error(`Retry time exhausted: ${this.url}`);
throw new Error('Retry time exhausted');
this.logger.error(`Retry time exhausted`);
return;
}
return this.getOptionsProp(this.options, 'retryDelay') || 0;
}

View File

@@ -111,9 +111,7 @@ export abstract class Server {
): Promise<any> {
const handler = this.getHandlerByPattern(pattern);
if (!handler) {
return this.logger.error(
`${NO_EVENT_HANDLER} Event pattern: ${JSON.stringify(pattern)}.`,
);
return this.logger.error(NO_EVENT_HANDLER`${pattern}`);
}
const resultOrStream = await handler(packet.data, context);
if (isObservable(resultOrStream)) {

View File

@@ -1,9 +1,7 @@
import { expect } from 'chai';
import { Subject } from 'rxjs';
import * as sinon from 'sinon';
import { ClientRedis } from '../../client/client-redis';
import { ERROR_EVENT } from '../../constants';
import { Client } from '../../external/nats-client.interface';
describe('ClientRedis', () => {
const test = 'test';
@@ -252,53 +250,39 @@ describe('ClientRedis', () => {
});
});
describe('getClientOptions', () => {
it('should return options object with "retry_strategy" and call "createRetryStrategy"', () => {
it('should return options object with "retryStrategy" and call "createRetryStrategy"', () => {
const createSpy = sinon.spy(client, 'createRetryStrategy');
const { retry_strategy } = client.getClientOptions(new Subject());
const { retryStrategy } = client.getClientOptions();
try {
retry_strategy({} as any);
retryStrategy({} as any);
} catch {}
expect(createSpy.called).to.be.true;
});
});
describe('createRetryStrategy', () => {
const subject = new Subject<Error>();
describe('when is terminated', () => {
it('should return undefined', () => {
(client as any).isExplicitlyTerminated = true;
const result = client.createRetryStrategy({} as any, subject);
const result = client.createRetryStrategy(0);
expect(result).to.be.undefined;
});
});
describe('when "retryAttempts" does not exist', () => {
it('should return an error', () => {
it('should return undefined', () => {
(client as any).isExplicitlyTerminated = false;
(client as any).options.options = {};
(client as any).options.options.retryAttempts = undefined;
const result = client.createRetryStrategy({} as any, subject);
expect(result).to.be.instanceOf(Error);
const result = client.createRetryStrategy(1);
expect(result).to.be.undefined;
});
});
describe('when "attempts" count is max', () => {
it('should return an error', () => {
it('should return undefined', () => {
(client as any).isExplicitlyTerminated = false;
(client as any).options.options = {};
(client as any).options.options.retryAttempts = 3;
const result = client.createRetryStrategy(
{ attempt: 4 } as any,
subject,
);
expect(result).to.be.instanceOf(Error);
});
});
describe('when ECONNREFUSED', () => {
it('should return error', () => {
(client as any).options.options = {};
(client as any).options.options.retryAttempts = 10;
const error = { code: 'ECONNREFUSED' };
const result = client.createRetryStrategy({ error } as any, subject);
expect(result).to.be.instanceOf(Error);
const result = client.createRetryStrategy(4);
expect(result).to.be.undefined;
});
});
describe('otherwise', () => {
@@ -307,10 +291,7 @@ describe('ClientRedis', () => {
(client as any).isExplicitlyTerminated = false;
(client as any).options.retryAttempts = 3;
(client as any).options.retryDelay = 3;
const result = client.createRetryStrategy(
{ attempt: 2 } as any,
subject,
);
const result = client.createRetryStrategy(2);
expect(result).to.be.eql((client as any).options.retryDelay);
});
});

View File

@@ -1,13 +1,15 @@
import { expect } from 'chai';
import { KafkaContext } from '../../ctx-host';
import { KafkaMessage } from '../../external/kafka.interface';
import { Consumer, KafkaMessage } from '../../external/kafka.interface';
describe('KafkaContext', () => {
const args = ['test', { test: true }];
const args = ['test', { test: true }, undefined, { test: 'consumer' }];
let context: KafkaContext;
beforeEach(() => {
context = new KafkaContext(args as [KafkaMessage, number, string]);
context = new KafkaContext(
args as [KafkaMessage, number, string, Consumer],
);
});
describe('getTopic', () => {
it('should return topic', () => {
@@ -24,4 +26,9 @@ describe('KafkaContext', () => {
expect(context.getMessage()).to.be.eql(args[0]);
});
});
describe('getConsumer', () => {
it('should return consumer instance', () => {
expect(context.getConsumer()).to.deep.eq({ test: 'consumer' });
});
});
});

View File

@@ -9,14 +9,24 @@ import { EventPattern } from '../../decorators/event-pattern.decorator';
describe('@EventPattern', () => {
const pattern = { role: 'test' };
const patternSecond = { role: 'test2' };
const patternThird = { role: 'test3' };
const extras = { param: 'value' };
class TestComponent {
@EventPattern(pattern, undefined, extras)
public static test() {}
@EventPattern(patternSecond, undefined, extras)
@EventPattern(patternThird, undefined, extras)
public static testOnlyThird() {}
@EventPattern([patternSecond, patternThird], undefined, extras)
public static testBoth() {}
}
it(`should enhance method with ${PATTERN_METADATA} metadata`, () => {
const metadata = Reflect.getMetadata(PATTERN_METADATA, TestComponent.test);
expect(metadata).to.be.eql(pattern);
expect(metadata.length).to.equal(1);
expect(metadata[0]).to.be.eql(pattern);
});
it(`should enhance method with ${PATTERN_EXTRAS_METADATA} metadata`, () => {
const metadata = Reflect.getMetadata(
@@ -25,6 +35,23 @@ describe('@EventPattern', () => {
);
expect(metadata).to.be.deep.equal(extras);
});
it(`should enhance method with last ${PATTERN_METADATA} metadata`, () => {
const metadata = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent.testOnlyThird,
);
expect(metadata.length).to.equal(1);
expect(metadata[0]).to.be.eql(patternSecond);
});
it(`should enhance method with both ${PATTERN_METADATA} metadata`, () => {
const metadata = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent.testBoth,
);
expect(metadata.length).to.equal(2);
expect(metadata[0]).to.be.eql(patternSecond);
expect(metadata[1]).to.be.eql(patternThird);
});
describe('decorator overloads', () => {
class TestComponent1 {
@@ -45,7 +72,7 @@ describe('@EventPattern', () => {
}
it(`should enhance method with ${PATTERN_METADATA} metadata`, () => {
const metadataArg = Reflect.getMetadata(
const [metadataArg] = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent1.test,
);
@@ -63,7 +90,7 @@ describe('@EventPattern', () => {
});
it(`should enhance method with ${PATTERN_METADATA}, ${TRANSPORT_METADATA} metadata`, () => {
const metadataArg = Reflect.getMetadata(
const [metadataArg] = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent2.test,
);
@@ -81,7 +108,7 @@ describe('@EventPattern', () => {
});
it(`should enhance method with ${PATTERN_METADATA}, ${PATTERN_EXTRAS_METADATA} metadata`, () => {
const metadataArg = Reflect.getMetadata(
const [metadataArg] = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent3.test,
);
@@ -100,7 +127,7 @@ describe('@EventPattern', () => {
it(`should enhance method with ${PATTERN_METADATA}, ${TRANSPORT_METADATA} and \
${PATTERN_EXTRAS_METADATA} metadata`, () => {
const metadataArg = Reflect.getMetadata(
const [metadataArg] = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent4.test,
);

View File

@@ -21,7 +21,10 @@ describe('@MessagePattern', () => {
public static test() {}
}
it(`should enhance method with ${PATTERN_METADATA} metadata`, () => {
const metadata = Reflect.getMetadata(PATTERN_METADATA, TestComponent.test);
const [metadata] = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent.test,
);
expect(metadata).to.be.eql(pattern);
});
it(`should enhance method with ${PATTERN_EXTRAS_METADATA} metadata`, () => {
@@ -51,7 +54,7 @@ describe('@MessagePattern', () => {
}
it(`should enhance method with ${PATTERN_METADATA} metadata`, () => {
const metadataArg = Reflect.getMetadata(
const [metadataArg] = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent1.test,
);
@@ -69,7 +72,7 @@ describe('@MessagePattern', () => {
});
it(`should enhance method with ${PATTERN_METADATA}, ${TRANSPORT_METADATA} metadata`, () => {
const metadataArg = Reflect.getMetadata(
const [metadataArg] = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent2.test,
);
@@ -87,7 +90,7 @@ describe('@MessagePattern', () => {
});
it(`should enhance method with ${PATTERN_METADATA}, ${PATTERN_EXTRAS_METADATA} metadata`, () => {
const metadataArg = Reflect.getMetadata(
const [metadataArg] = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent3.test,
);
@@ -106,7 +109,7 @@ describe('@MessagePattern', () => {
it(`should enhance method with ${PATTERN_METADATA}, ${TRANSPORT_METADATA} and \
${PATTERN_EXTRAS_METADATA} metadata`, () => {
const metadataArg = Reflect.getMetadata(
const [metadataArg] = Reflect.getMetadata(
PATTERN_METADATA,
TestComponent4.test,
);
@@ -139,7 +142,7 @@ describe('@GrpcMethod', () => {
it('should derive method and service name', () => {
const svc = new TestService();
const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test);
const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test);
expect(metadata).to.be.eql({
service: TestService.name,
rpc: 'Test',
@@ -149,7 +152,7 @@ describe('@GrpcMethod', () => {
it('should derive method', () => {
const svc = new TestService();
const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test2);
const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test2);
expect(metadata).to.be.eql({
service: 'TestService2',
rpc: 'Test2',
@@ -159,7 +162,7 @@ describe('@GrpcMethod', () => {
it('should override both method and service', () => {
const svc = new TestService();
const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test3);
const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test3);
expect(metadata).to.be.eql({
service: 'TestService2',
rpc: 'Test2',
@@ -182,7 +185,7 @@ describe('@GrpcStreamMethod', () => {
it('should derive method and service name', () => {
const svc = new TestService();
const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test);
const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test);
expect(metadata).to.be.eql({
service: TestService.name,
rpc: 'Test',
@@ -192,7 +195,7 @@ describe('@GrpcStreamMethod', () => {
it('should derive method', () => {
const svc = new TestService();
const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test2);
const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test2);
expect(metadata).to.be.eql({
service: 'TestService2',
rpc: 'Test2',
@@ -202,7 +205,7 @@ describe('@GrpcStreamMethod', () => {
it('should override both method and service', () => {
const svc = new TestService();
const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test3);
const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test3);
expect(metadata).to.be.eql({
service: 'TestService2',
rpc: 'Test2',
@@ -225,7 +228,7 @@ describe('@GrpcStreamCall', () => {
it('should derive method and service name', () => {
const svc = new TestService();
const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test);
const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test);
expect(metadata).to.be.eql({
service: TestService.name,
rpc: 'Test',
@@ -235,7 +238,7 @@ describe('@GrpcStreamCall', () => {
it('should derive method', () => {
const svc = new TestService();
const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test2);
const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test2);
expect(metadata).to.be.eql({
service: 'TestService2',
rpc: 'Test2',
@@ -245,7 +248,7 @@ describe('@GrpcStreamCall', () => {
it('should override both method and service', () => {
const svc = new TestService();
const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test3);
const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test3);
expect(metadata).to.be.eql({
service: 'TestService2',
rpc: 'Test2',

View File

@@ -74,8 +74,8 @@ describe('ListenersController', () => {
describe('registerPatternHandlers', () => {
const handlers = [
{ pattern: 'test', targetCallback: 'tt' },
{ pattern: 'test2', targetCallback: '2', isEventHandler: true },
{ patterns: ['test'], targetCallback: 'tt' },
{ patterns: ['test2'], targetCallback: '2', isEventHandler: true },
];
beforeEach(() => {
@@ -89,7 +89,7 @@ describe('ListenersController', () => {
it(`should call "addHandler" method of server for each pattern handler with same transport`, () => {
const serverHandlers = [
{
pattern: { cmd: 'test' },
patterns: [{ cmd: 'test' }],
targetCallback: 'tt',
transport: Transport.TCP,
},
@@ -101,8 +101,12 @@ describe('ListenersController', () => {
});
it(`should call "addHandler" method of server without transportID for each pattern handler with any transport value`, () => {
const serverHandlers = [
{ pattern: { cmd: 'test' }, targetCallback: 'tt' },
{ pattern: 'test2', targetCallback: '2', transport: Transport.KAFKA },
{ patterns: [{ cmd: 'test' }], targetCallback: 'tt' },
{
patterns: ['test2'],
targetCallback: '2',
transport: Transport.KAFKA,
},
];
explorer.expects('explore').returns(serverHandlers);
instance.registerPatternHandlers(new InstanceWrapper(), server, '');
@@ -110,10 +114,14 @@ describe('ListenersController', () => {
});
it(`should call "addHandler" method of server with transportID for each pattern handler with self transport and without transport`, () => {
const serverHandlers = [
{ pattern: 'test', targetCallback: 'tt' },
{ pattern: 'test2', targetCallback: '2', transport: Transport.KAFKA },
{ patterns: ['test'], targetCallback: 'tt' },
{
pattern: { cmd: 'test3' },
patterns: ['test2'],
targetCallback: '2',
transport: Transport.KAFKA,
},
{
patterns: [{ cmd: 'test3' }],
targetCallback: '3',
transport: Transport.TCP,
},
@@ -130,11 +138,15 @@ describe('ListenersController', () => {
it(`should call "addHandler" method of server with custom transportID for pattern handler with the same custom token`, () => {
const serverHandlers = [
{
pattern: { cmd: 'test' },
patterns: [{ cmd: 'test' }],
targetCallback: 'tt',
transport: customTransport,
},
{ pattern: 'test2', targetCallback: '2', transport: Transport.KAFKA },
{
patterns: ['test2'],
targetCallback: '2',
transport: Transport.KAFKA,
},
];
explorer.expects('explore').returns(serverHandlers);
@@ -143,7 +155,11 @@ describe('ListenersController', () => {
});
it(`should call "addHandler" method of server with extras data`, () => {
const serverHandlers = [
{ pattern: 'test', targetCallback: 'tt', extras: { param: 'value' } },
{
patterns: ['test'],
targetCallback: 'tt',
extras: { param: 'value' },
},
];
explorer.expects('explore').returns(serverHandlers);
instance.registerPatternHandlers(new InstanceWrapper(), serverTCP, '');
@@ -193,7 +209,7 @@ describe('ListenersController', () => {
const module = {
controllers: new Map(),
} as any;
const pattern = {};
const patterns = [{}];
const wrapper = new InstanceWrapper({ instance: { [methodKey]: {} } });
it('should pass all arguments to the proxy chain', async () => {
@@ -202,7 +218,7 @@ describe('ListenersController', () => {
.callsFake(() => Promise.resolve({}));
const handler = instance.createRequestScopedHandler(
wrapper,
pattern,
patterns,
module,
moduleKey,
methodKey,
@@ -221,7 +237,7 @@ describe('ListenersController', () => {
const module = {
controllers: new Map(),
} as any;
const pattern = {};
const patterns = [{}];
const wrapper = new InstanceWrapper({ instance: { [methodKey]: {} } });
it('should delegete error to exception filters', async () => {
@@ -230,7 +246,7 @@ describe('ListenersController', () => {
});
const handler = instance.createRequestScopedHandler(
wrapper,
pattern,
patterns,
module,
moduleKey,
methodKey,

View File

@@ -2,15 +2,20 @@ import { expect } from 'chai';
import * as sinon from 'sinon';
import { MetadataScanner } from '../../core/metadata-scanner';
import { Client } from '../decorators/client.decorator';
import { EventPattern } from '../decorators/event-pattern.decorator';
import { MessagePattern } from '../decorators/message-pattern.decorator';
import { Transport } from '../enums/transport.enum';
import { ListenerMetadataExplorer } from '../listener-metadata-explorer';
describe('ListenerMetadataExplorer', () => {
const pattern = { pattern: 'test' };
const secPattern = { role: '2', cmd: 'm' };
const msgPattern = { pattern: 'testMsg' };
const firstMultipleMsgPattern = { pattern: 'testMultipleMsg1' };
const secondMultipleMsgPattern = { pattern: 'testMultipleMsg2' };
const clientMetadata = {};
const clientSecMetadata = { transport: Transport.REDIS };
const evtPattern = { role: 'testEvt' };
const firstMultipleEvtPattern = { role: 'testMultipleEvt1' };
const secondMultipleEvtPattern = { role: 'testMultipleEvt2' };
class Test {
@Client(clientMetadata as any)
@@ -25,11 +30,17 @@ describe('ListenerMetadataExplorer', () => {
constructor() {}
@MessagePattern(pattern)
public test() {}
@MessagePattern(msgPattern)
public testMessage() {}
@MessagePattern(secPattern)
public testSec() {}
@MessagePattern([firstMultipleMsgPattern, secondMultipleMsgPattern])
public testMultipleMessage() {}
@EventPattern(evtPattern)
public testEvent() {}
@EventPattern([firstMultipleEvtPattern, secondMultipleEvtPattern])
public testMultipleEvent() {}
public noPattern() {}
}
@@ -66,20 +77,77 @@ describe('ListenerMetadataExplorer', () => {
);
expect(metadata).to.eq(undefined);
});
it(`should return pattern properties when "handlerType" metadata is not undefined`, () => {
const metadata = instance.exploreMethodMetadata(
Object.getPrototypeOf(test),
'test',
);
expect(metadata).to.have.keys([
'isEventHandler',
'methodKey',
'targetCallback',
'pattern',
'transport',
'extras',
]);
expect(metadata.pattern).to.eql(pattern);
describe('@MessagePattern', () => {
it(`should return pattern properties when "handlerType" metadata is not undefined`, () => {
const metadata = instance.exploreMethodMetadata(
Object.getPrototypeOf(test),
'testMessage',
);
expect(metadata).to.have.keys([
'isEventHandler',
'methodKey',
'targetCallback',
'patterns',
'transport',
'extras',
]);
expect(metadata.patterns.length).to.eql(1);
expect(metadata.patterns[0]).to.eql(msgPattern);
});
it(`should return multiple patterns when more than one is declared`, () => {
const metadata = instance.exploreMethodMetadata(
Object.getPrototypeOf(test),
'testMultipleMessage',
);
expect(metadata).to.have.keys([
'isEventHandler',
'methodKey',
'targetCallback',
'patterns',
'transport',
'extras',
]);
expect(metadata.patterns.length).to.eql(2);
expect(metadata.patterns[0]).to.eql(firstMultipleMsgPattern);
expect(metadata.patterns[1]).to.eql(secondMultipleMsgPattern);
});
});
describe('@EventPattern', () => {
it(`should return pattern properties when "handlerType" metadata is not undefined`, () => {
const metadata = instance.exploreMethodMetadata(
Object.getPrototypeOf(test),
'testEvent',
);
expect(metadata).to.have.keys([
'isEventHandler',
'methodKey',
'targetCallback',
'patterns',
'transport',
'extras',
]);
expect(metadata.patterns.length).to.eql(1);
expect(metadata.patterns[0]).to.eql(evtPattern);
});
it(`should return multiple patterns when more than one is declared`, () => {
const metadata = instance.exploreMethodMetadata(
Object.getPrototypeOf(test),
'testMultipleEvent',
);
expect(metadata).to.have.keys([
'isEventHandler',
'methodKey',
'targetCallback',
'patterns',
'transport',
'extras',
]);
expect(metadata.patterns.length).to.eql(2);
expect(metadata.patterns[0]).to.eql(firstMultipleEvtPattern);
expect(metadata.patterns[1]).to.eql(secondMultipleEvtPattern);
});
});
});
describe('scanForClientHooks', () => {

View File

@@ -1,5 +1,5 @@
import { Logger } from '@nestjs/common';
import { expect } from 'chai';
import { AssertionError, expect } from 'chai';
import * as sinon from 'sinon';
import { NO_MESSAGE_HANDLER } from '../../constants';
import { KafkaHeaders } from '../../enums';
@@ -277,6 +277,7 @@ describe('ServerKafka', () => {
sinon.stub(server, 'getPublisher').callsFake(() => getPublisherSpy);
});
it('should call "handleEvent" if correlation identifier is not present', async () => {
const handleEventSpy = sinon.spy(server, 'handleEvent');
await server.handleMessage(eventPayload);
@@ -289,6 +290,42 @@ describe('ServerKafka', () => {
expect(handleEventSpy.called).to.be.true;
});
it('should call event handler when "handleEvent" is called', async () => {
const messageHandler = sinon.mock();
const context = { test: true } as any;
const messageData = 'some data';
sinon.stub(server, 'getHandlerByPattern').callsFake(() => messageHandler);
await server.handleEvent(
topic,
{ data: messageData, pattern: topic },
context,
);
expect(messageHandler.calledWith(messageData, context)).to.be.true;
});
it('should not catch error thrown by event handler as part of "handleEvent"', async () => {
const error = new Error('handler error');
const messageHandler = sinon.mock().throwsException(error);
sinon.stub(server, 'getHandlerByPattern').callsFake(() => messageHandler);
try {
await server.handleEvent(
topic,
{ data: 'some data', pattern: topic },
{} as any,
);
// code should not be executed
expect(true).to.be.false;
} catch (e) {
if (e instanceof AssertionError) {
throw e;
}
expect(e).to.be.eq(error);
}
});
it('should call "handleEvent" if correlation identifier and reply topic are present but the handler is of type eventHandler', async () => {
const handler = sinon.spy();
(handler as any).isEventHandler = true;
@@ -320,6 +357,7 @@ describe('ServerKafka', () => {
}),
).to.be.true;
});
it(`should call handler with expected arguments`, async () => {
const handler = sinon.spy();
(server as any).messageHandlers = objectToMap({

View File

@@ -15,13 +15,17 @@ describe('ServerRedis', () => {
});
describe('listen', () => {
let onSpy: sinon.SinonSpy;
let connectSpy: sinon.SinonSpy;
let client: any;
let callbackSpy: sinon.SinonSpy;
beforeEach(() => {
onSpy = sinon.spy();
connectSpy = sinon.spy();
client = {
on: onSpy,
connect: connectSpy,
};
sinon.stub(server, 'createRedisClient').callsFake(() => client);
@@ -31,13 +35,9 @@ describe('ServerRedis', () => {
server.listen(callbackSpy);
expect(onSpy.getCall(0).args[0]).to.be.equal('error');
});
it('should bind "connect" event to handler', () => {
it('should call "RedisClient#connect()"', () => {
server.listen(callbackSpy);
expect(onSpy.getCall(3).args[0]).to.be.equal('connect');
});
it('should bind "message" event to handler', () => {
server.listen(callbackSpy);
expect(onSpy.getCall(2).args[0]).to.be.equal('message');
expect(connectSpy.called).to.be.true;
});
describe('when "start" throws an exception', () => {
it('should call callback with a thrown error as an argument', () => {
@@ -191,11 +191,11 @@ describe('ServerRedis', () => {
});
});
describe('getClientOptions', () => {
it('should return options object with "retry_strategy" and call "createRetryStrategy"', () => {
it('should return options object with "retryStrategy" and call "createRetryStrategy"', () => {
const createSpy = sinon.spy(server, 'createRetryStrategy');
const { retry_strategy } = server.getClientOptions();
const { retryStrategy } = server.getClientOptions();
try {
retry_strategy({} as any);
retryStrategy(0);
} catch {}
expect(createSpy.called).to.be.true;
});
@@ -204,37 +204,24 @@ describe('ServerRedis', () => {
describe('when is terminated', () => {
it('should return undefined', () => {
(server as any).isExplicitlyTerminated = true;
const result = server.createRetryStrategy({} as any);
const result = server.createRetryStrategy(0);
expect(result).to.be.undefined;
});
});
describe('when "retryAttempts" does not exist', () => {
it('should throw an exception', () => {
it('should return undefined', () => {
(server as any).options.options = {};
(server as any).options.options.retryAttempts = undefined;
expect(() => server.createRetryStrategy({} as any)).to.throw(Error);
expect(server.createRetryStrategy(4)).to.be.undefined;
});
});
describe('when "attempts" count is max', () => {
it('should throw an exception', () => {
it('should return undefined', () => {
(server as any).options.options = {};
(server as any).options.options.retryAttempts = 3;
expect(() =>
server.createRetryStrategy({ attempt: 4 } as any),
).to.throw(Error);
});
});
describe('when ECONNREFUSED', () => {
it('should call logger', () => {
const loggerErrorSpy = sinon.spy((server as any).logger, 'error');
try {
server.createRetryStrategy({
error: { code: 'ECONNREFUSED' },
} as any);
} catch {}
expect(loggerErrorSpy.called).to.be.true;
expect(server.createRetryStrategy(4)).to.be.undefined;
});
});
describe('otherwise', () => {
@@ -243,7 +230,7 @@ describe('ServerRedis', () => {
(server as any).isExplicitlyTerminated = false;
(server as any).options.retryAttempts = 3;
(server as any).options.retryDelay = 3;
const result = server.createRetryStrategy({ attempt: 2 } as any);
const result = server.createRetryStrategy(2);
expect(result).to.be.eql((server as any).options.retryDelay);
});
});

View File

@@ -382,8 +382,8 @@ export class FastifyAdapter<
public useStaticAssets(options: FastifyStaticOptions) {
return this.register(
loadPackage('fastify-static', 'FastifyAdapter.useStaticAssets()', () =>
require('fastify-static'),
loadPackage('@fastify/static', 'FastifyAdapter.useStaticAssets()', () =>
require('@fastify/static'),
),
options,
);
@@ -423,14 +423,14 @@ export class FastifyAdapter<
}
public enableCors(options: CorsOptions | CorsOptionsDelegate<TRequest>) {
this.register(import('fastify-cors'), options);
this.register(import('@fastify/cors'), options);
}
public registerParserMiddleware(prefix?: string, rawBody?: boolean) {
if (this._isParserRegistered) {
return;
}
this.register(import('fastify-formbody'));
this.register(import('@fastify/formbody'));
if (rawBody) {
this.registerContentParserWithRawBody();

View File

@@ -2,15 +2,28 @@
* "fastify-static" interfaces
* @see https://github.com/fastify/fastify-static/blob/master/index.d.ts
*/
import { Stats } from 'fs';
interface ExtendedInformation {
fileCount: number;
totalFileCount: number;
folderCount: number;
totalFolderCount: number;
totalSize: number;
lastModified: number;
}
interface ListDir {
href: string;
name: string;
stats: Stats;
extendedInfo?: ExtendedInformation;
}
interface ListFile {
href: string;
name: string;
stats: Stats;
}
interface ListRender {
@@ -21,6 +34,8 @@ interface ListOptions {
format: 'json' | 'html';
names: string[];
render: ListRender;
extendedFolderInfo?: boolean;
jsonFormat?: 'names' | 'extended';
}
// Passed on to `send`
@@ -48,6 +63,11 @@ export interface FastifyStaticOptions extends SendOptions {
wildcard?: boolean;
list?: boolean | ListOptions;
allowedPath?: (pathName: string, root?: string) => boolean;
/**
* @description
* Opt-in to looking for pre-compressed files
*/
preCompressed?: boolean;
// Passed on to `send`
acceptRanges?: boolean;

View File

@@ -9,7 +9,6 @@ export interface PointOfViewOptions {
nunjucks?: any;
pug?: any;
handlebars?: any;
marko?: any;
mustache?: any;
'art-template'?: any;
twig?: any;

View File

@@ -16,7 +16,7 @@ import { FastifyStaticOptions, PointOfViewOptions } from './external';
export interface NestFastifyApplication extends INestApplication {
/**
* A wrapper function around native `fastify.register()` method.
* Example `app.register(require('fastify-formbody'))
* Example `app.register(require('@fastify/formbody'))
* @returns {Promise<FastifyInstance>}
*/
register<Options extends FastifyPluginOptions = any>(

View File

@@ -18,8 +18,8 @@
},
"dependencies": {
"fastify": "3.29.0",
"fastify-cors": "6.1.0",
"fastify-formbody": "5.3.0",
"@fastify/cors": "7.0.0",
"@fastify/formbody": "6.0.0",
"light-my-request": "4.10.1",
"middie": "6.1.0",
"path-to-regexp": "3.2.0",
@@ -27,6 +27,16 @@
},
"peerDependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0"
"@nestjs/core": "^8.0.0",
"@fastify/static": "^5.0.0",
"point-of-view": "^5.0.0"
},
"peerDependenciesMeta": {
"@fastify/static": {
"optional": true
},
"point-of-view": {
"optional": true
}
}
}

View File

@@ -345,7 +345,7 @@ describe('WebSocketsController', () => {
Promise.resolve(Promise.resolve(value)),
),
),
).to.be.eq(100);
).to.be.eq(value);
});
});
@@ -356,18 +356,29 @@ describe('WebSocketsController', () => {
await lastValueFrom(
await instance.pickResult(Promise.resolve(of(value))),
),
).to.be.eq(100);
).to.be.eq(value);
});
});
describe('is a value', () => {
describe('is an object that has the method `subscribe`', () => {
it('should return Promise<Observable>', async () => {
const value = { subscribe() {} };
expect(
await lastValueFrom(
await instance.pickResult(Promise.resolve(value)),
),
).to.equal(value);
});
});
describe('is an ordinary value', () => {
it('should return Promise<Observable>', async () => {
const value = 100;
expect(
await lastValueFrom(
await instance.pickResult(Promise.resolve(value)),
),
).to.be.eq(100);
).to.be.eq(value);
});
});
});

View File

@@ -1,9 +1,14 @@
import { Type } from '@nestjs/common/interfaces/type.interface';
import { Logger } from '@nestjs/common/services/logger.service';
import { isFunction } from '@nestjs/common/utils/shared.utils';
import { ApplicationConfig } from '@nestjs/core/application-config';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { from as fromPromise, Observable, of, Subject } from 'rxjs';
import {
from as fromPromise,
Observable,
isObservable,
of,
Subject,
} from 'rxjs';
import { distinctUntilChanged, mergeAll } from 'rxjs/operators';
import { GATEWAY_OPTIONS, PORT_METADATA } from './constants';
import { WsContextCreator } from './context/ws-context-creator';
@@ -158,7 +163,7 @@ export class WebSocketsController {
deferredResult: Promise<any>,
): Promise<Observable<any>> {
const result = await deferredResult;
if (result && isFunction(result.subscribe)) {
if (isObservable(result)) {
return result;
}
if (result instanceof Promise) {

View File

@@ -845,6 +845,30 @@
"ajv": "^6.12.6"
}
},
"@fastify/static": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-5.0.0.tgz",
"integrity": "sha512-GGltJkO0idXa7yCZ0PfdTZ6qokWDX/vigCvmRpjOU2A3jc93c9p+oHDvHmwHK60hwWoBEGqHjGofVyn3H1CjZg==",
"requires": {
"content-disposition": "^0.5.3",
"encoding-negotiator": "^2.0.1",
"fastify-plugin": "^3.0.0",
"glob": "^7.1.4",
"p-limit": "^3.1.0",
"readable-stream": "^3.4.0",
"send": "^0.17.1"
},
"dependencies": {
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"requires": {
"yocto-queue": "^0.1.0"
}
}
}
},
"@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
@@ -4255,30 +4279,6 @@
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz",
"integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w=="
},
"fastify-static": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/fastify-static/-/fastify-static-4.5.0.tgz",
"integrity": "sha512-Q7Tgl55AjsmBwiO4hKYib2BUCt+XTWLJ6Xp8YPPHU3EsrKNpevJ4cz8pjf1Ey1QhHw9O8Y2FDKdu+IC74oHvqw==",
"requires": {
"content-disposition": "^0.5.3",
"encoding-negotiator": "^2.0.1",
"fastify-plugin": "^3.0.0",
"glob": "^7.1.4",
"p-limit": "^3.1.0",
"readable-stream": "^3.4.0",
"send": "^0.17.1"
},
"dependencies": {
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"requires": {
"yocto-queue": "^0.1.0"
}
}
}
},
"fastify-warning": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz",

View File

@@ -22,7 +22,7 @@
"@nestjs/common": "8.2.3",
"@nestjs/core": "8.2.3",
"@nestjs/platform-fastify": "8.2.3",
"fastify-static": "4.5.0",
"@fastify/static": "5.0.0",
"handlebars": "4.7.7",
"point-of-view": "4.15.3",
"reflect-metadata": "0.1.13",