mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 15:08:37 +00:00
Merge branch '8.0.0' into fix-set-name-property-of-exception-object-equal-to-class-name
This commit is contained in:
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2017-2020 Kamil Mysliwiec <https://kamilmysliwiec.com>
|
||||
Copyright (c) 2017-2021 Kamil Mysliwiec <https://kamilmysliwiec.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
177
integration/cors/e2e/express.spec.ts
Normal file
177
integration/cors/e2e/express.spec.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('Express Cors', () => {
|
||||
let app: NestFastifyApplication;
|
||||
const configs = [
|
||||
{
|
||||
origin: 'example.com',
|
||||
methods: 'GET',
|
||||
credentials: true,
|
||||
exposedHeaders: ['foo', 'bar'],
|
||||
allowedHeaders: ['baz', 'woo'],
|
||||
maxAge: 123,
|
||||
},
|
||||
{
|
||||
origin: 'sample.com',
|
||||
methods: 'GET',
|
||||
credentials: true,
|
||||
exposedHeaders: ['zoo', 'bar'],
|
||||
allowedHeaders: ['baz', 'foo'],
|
||||
maxAge: 321,
|
||||
},
|
||||
];
|
||||
describe('Dynamic config', () => {
|
||||
describe('enableCors', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>();
|
||||
|
||||
let requestId = 0;
|
||||
const configDelegation = function (req, cb) {
|
||||
const config = configs[requestId];
|
||||
requestId++;
|
||||
cb(null, config);
|
||||
};
|
||||
app.enableCors(configDelegation);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the first config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the second config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.options('/')
|
||||
.expect('access-control-allow-origin', 'sample.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'zoo,bar')
|
||||
.expect('access-control-allow-methods', 'GET')
|
||||
.expect('access-control-allow-headers', 'baz,foo')
|
||||
.expect('access-control-max-age', '321')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Application Options', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
let requestId = 0;
|
||||
const configDelegation = function (req, cb) {
|
||||
const config = configs[requestId];
|
||||
requestId++;
|
||||
cb(null, config);
|
||||
};
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>(null, {
|
||||
cors: configDelegation,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the first config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the second config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.options('/')
|
||||
.expect('access-control-allow-origin', 'sample.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'zoo,bar')
|
||||
.expect('access-control-allow-methods', 'GET')
|
||||
.expect('access-control-allow-headers', 'baz,foo')
|
||||
.expect('access-control-max-age', '321')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Static config', () => {
|
||||
describe('enableCors', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>();
|
||||
app.enableCors(configs[0]);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`CORS headers`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('Application Options', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>(null, {
|
||||
cors: configs[0],
|
||||
});
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`CORS headers`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
177
integration/cors/e2e/fastify.spec.ts
Normal file
177
integration/cors/e2e/fastify.spec.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('Fastify Cors', () => {
|
||||
let app: NestFastifyApplication;
|
||||
const configs = [
|
||||
{
|
||||
origin: 'example.com',
|
||||
methods: 'GET',
|
||||
credentials: true,
|
||||
exposedHeaders: ['foo', 'bar'],
|
||||
allowedHeaders: ['baz', 'woo'],
|
||||
maxAge: 123,
|
||||
},
|
||||
{
|
||||
origin: 'sample.com',
|
||||
methods: 'GET',
|
||||
credentials: true,
|
||||
exposedHeaders: ['zoo', 'bar'],
|
||||
allowedHeaders: ['baz', 'foo'],
|
||||
maxAge: 321,
|
||||
},
|
||||
];
|
||||
describe('Dynamic config', () => {
|
||||
describe('enableCors', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>();
|
||||
|
||||
let requestId = 0;
|
||||
const configDelegation = function (req, cb) {
|
||||
const config = configs[requestId];
|
||||
requestId++;
|
||||
cb(null, config);
|
||||
};
|
||||
app.enableCors(configDelegation);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the first config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the second config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.options('/')
|
||||
.expect('access-control-allow-origin', 'sample.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'zoo,bar')
|
||||
.expect('access-control-allow-methods', 'GET')
|
||||
.expect('access-control-allow-headers', 'baz,foo')
|
||||
.expect('access-control-max-age', '321')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Application Options', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
let requestId = 0;
|
||||
const configDelegation = function (req, cb) {
|
||||
const config = configs[requestId];
|
||||
requestId++;
|
||||
cb(null, config);
|
||||
};
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>(null, {
|
||||
cors: configDelegation,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the first config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
it(`Should add cors headers based on the second config`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.options('/')
|
||||
.expect('access-control-allow-origin', 'sample.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'zoo,bar')
|
||||
.expect('access-control-allow-methods', 'GET')
|
||||
.expect('access-control-allow-headers', 'baz,foo')
|
||||
.expect('access-control-max-age', '321')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Static config', () => {
|
||||
describe('enableCors', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>();
|
||||
app.enableCors(configs[0]);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`CORS headers`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
describe('Application Options', () => {
|
||||
before(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication<NestFastifyApplication>(null, {
|
||||
cors: configs[0],
|
||||
});
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`CORS headers`, async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect('access-control-allow-origin', 'example.com')
|
||||
.expect('vary', 'Origin')
|
||||
.expect('access-control-allow-credentials', 'true')
|
||||
.expect('access-control-expose-headers', 'foo,bar')
|
||||
.expect('content-length', '0');
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
9
integration/cors/src/app.controller.ts
Normal file
9
integration/cors/src/app.controller.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@Get()
|
||||
getGlobals() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
7
integration/cors/src/app.module.ts
Normal file
7
integration/cors/src/app.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule {}
|
||||
22
integration/cors/tsconfig.json
Normal file
22
integration/cors/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": false,
|
||||
"noImplicitAny": false,
|
||||
"removeComments": true,
|
||||
"noLib": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"e2e/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
]
|
||||
}
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
- "9001:9001"
|
||||
restart: always
|
||||
mysql:
|
||||
image: mysql:5.7.32
|
||||
image: mysql:5.7.33
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: test
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
zookeeper:
|
||||
container_name: test-zookeeper
|
||||
hostname: zookeeper
|
||||
image: confluentinc/cp-zookeeper:5.5.2
|
||||
image: confluentinc/cp-zookeeper:5.5.3
|
||||
ports:
|
||||
- "2181:2181"
|
||||
environment:
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
kafka:
|
||||
container_name: test-kafka
|
||||
hostname: kafka
|
||||
image: confluentinc/cp-kafka:5.5.2
|
||||
image: confluentinc/cp-kafka:5.5.3
|
||||
depends_on:
|
||||
- zookeeper
|
||||
ports:
|
||||
@@ -70,3 +70,4 @@ services:
|
||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
|
||||
KAFKA_DELETE_TOPIC_ENABLE: 'true'
|
||||
|
||||
@@ -26,7 +26,7 @@ class TestInjectable
|
||||
class AppModule {}
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, { logger: true });
|
||||
const app = await NestFactory.create(AppModule, { logger: false });
|
||||
|
||||
if (SIGNAL_TO_LISTEN && SIGNAL_TO_LISTEN !== 'NONE') {
|
||||
app.enableShutdownHooks([SIGNAL_TO_LISTEN]);
|
||||
|
||||
291
integration/microservices/e2e/concurrent-kafka.spec.ts
Normal file
291
integration/microservices/e2e/concurrent-kafka.spec.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { INestApplication, Logger } from '@nestjs/common';
|
||||
import { Transport } from '@nestjs/microservices';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { Admin, ITopicMetadata, Kafka } from 'kafkajs';
|
||||
import * as request from 'supertest';
|
||||
import * as util from 'util';
|
||||
import { KafkaConcurrentController } from '../src/kafka-concurrent/kafka-concurrent.controller';
|
||||
import { KafkaConcurrentMessagesController } from '../src/kafka-concurrent/kafka-concurrent.messages.controller';
|
||||
|
||||
describe('Kafka concurrent', function () {
|
||||
const numbersOfServers = 3;
|
||||
|
||||
const requestTopic = 'math.sum.sync.number.wait';
|
||||
const responseTopic = 'math.sum.sync.number.wait.reply';
|
||||
|
||||
let admin: Admin;
|
||||
const servers: any[] = [];
|
||||
const apps: INestApplication[] = [];
|
||||
|
||||
const logger = new Logger('concurrent-kafka.spec.ts');
|
||||
|
||||
// set timeout to be longer (especially for the after hook)
|
||||
this.timeout(30000);
|
||||
|
||||
const startServer = async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [
|
||||
KafkaConcurrentController,
|
||||
KafkaConcurrentMessagesController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
// use our own logger for a little
|
||||
// Logger.overrideLogger(new Logger());
|
||||
|
||||
const app = module.createNestApplication();
|
||||
|
||||
const server = app.getHttpAdapter().getInstance();
|
||||
|
||||
app.connectMicroservice({
|
||||
transport: Transport.KAFKA,
|
||||
options: {
|
||||
client: {
|
||||
brokers: ['localhost:9092'],
|
||||
},
|
||||
run: {
|
||||
partitionsConsumedConcurrently: numbersOfServers,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// enable these for clean shutdown
|
||||
app.enableShutdownHooks();
|
||||
|
||||
// push to the collection
|
||||
servers.push(server);
|
||||
apps.push(app);
|
||||
|
||||
// await the start
|
||||
await app.startAllMicroservicesAsync();
|
||||
await app.init();
|
||||
};
|
||||
|
||||
it(`Create kafka topics/partitions`, async () => {
|
||||
const kafka = new Kafka({
|
||||
clientId: 'concurrent-test-admin',
|
||||
brokers: ['localhost:9092'],
|
||||
});
|
||||
|
||||
admin = kafka.admin();
|
||||
await admin.connect();
|
||||
|
||||
let topicMetadata: {
|
||||
topics: ITopicMetadata[];
|
||||
};
|
||||
|
||||
try {
|
||||
topicMetadata = await admin.fetchTopicMetadata({
|
||||
topics: [requestTopic, responseTopic],
|
||||
});
|
||||
} catch (e) {
|
||||
// create with number of servers
|
||||
try {
|
||||
await admin.createTopics({
|
||||
topics: [
|
||||
{
|
||||
topic: requestTopic,
|
||||
numPartitions: numbersOfServers,
|
||||
replicationFactor: 1,
|
||||
},
|
||||
{
|
||||
topic: responseTopic,
|
||||
numPartitions: numbersOfServers,
|
||||
replicationFactor: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(util.format('Create topics error: %o', e));
|
||||
}
|
||||
}
|
||||
|
||||
if (topicMetadata && topicMetadata.topics.length > 0) {
|
||||
// we have topics, how many partitions do they have?
|
||||
for (const topic of topicMetadata.topics) {
|
||||
if (topic.partitions.length < numbersOfServers) {
|
||||
try {
|
||||
await admin.createPartitions({
|
||||
topicPartitions: [
|
||||
{
|
||||
topic: topic.name,
|
||||
count: numbersOfServers,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(util.format('Create partitions error: %o', e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create with number of servers
|
||||
try {
|
||||
await admin.createTopics({
|
||||
topics: [
|
||||
{
|
||||
topic: requestTopic,
|
||||
numPartitions: numbersOfServers,
|
||||
replicationFactor: 1,
|
||||
},
|
||||
{
|
||||
topic: responseTopic,
|
||||
numPartitions: numbersOfServers,
|
||||
replicationFactor: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(util.format('Create topics error: %o', e));
|
||||
}
|
||||
|
||||
// disconnect
|
||||
await admin.disconnect();
|
||||
});
|
||||
|
||||
it(`Start Kafka apps`, async () => {
|
||||
// start all at once
|
||||
await Promise.all(
|
||||
Array(numbersOfServers)
|
||||
.fill(1)
|
||||
.map(async (v, i) => {
|
||||
// return startServer();
|
||||
|
||||
// wait in intervals so the consumers start in order
|
||||
return new Promise<void>(resolve => {
|
||||
setTimeout(async () => {
|
||||
await startServer();
|
||||
|
||||
return resolve();
|
||||
}, 1000 * i);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}).timeout(30000);
|
||||
|
||||
it(`Concurrent messages without forcing a rebalance.`, async () => {
|
||||
// wait a second before notifying the servers to respond
|
||||
setTimeout(async () => {
|
||||
// notify the other servers that it is time to respond
|
||||
await Promise.all(
|
||||
servers.map(async server => {
|
||||
// send to all servers since indexes don't necessarily align with server consumers
|
||||
return request(server).post('/go').send();
|
||||
}),
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
await Promise.all(
|
||||
servers.map(async (server, index) => {
|
||||
// send requests
|
||||
const payload = {
|
||||
key: index,
|
||||
numbers: [1, index],
|
||||
};
|
||||
const result = (1 + index).toString();
|
||||
|
||||
return request(server)
|
||||
.post('/mathSumSyncNumberWait')
|
||||
.send(payload)
|
||||
.expect(200)
|
||||
.expect(200, result);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it(`Close kafka client consumer while waiting for message pattern response.`, async () => {
|
||||
await Promise.all(
|
||||
servers.map(async (server, index) => {
|
||||
// shut off and delete the leader
|
||||
if (index === 0) {
|
||||
return new Promise<void>(resolve => {
|
||||
// wait a second before closing so the producers can send the message to the server consumers
|
||||
setTimeout(async () => {
|
||||
// get the controller
|
||||
const controller = apps[index].get(KafkaConcurrentController);
|
||||
|
||||
// close the controller clients
|
||||
await controller.client.close();
|
||||
|
||||
// notify the other servers that we have stopped
|
||||
await Promise.all(
|
||||
servers.map(async server => {
|
||||
// send to all servers since indexes don't necessarily align with server consumers
|
||||
return request(server).post('/go').send();
|
||||
}),
|
||||
);
|
||||
|
||||
return resolve();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// send requests
|
||||
const payload = {
|
||||
key: index,
|
||||
numbers: [1, index],
|
||||
};
|
||||
const result = (1 + index).toString();
|
||||
|
||||
return request(server)
|
||||
.post('/mathSumSyncNumberWait')
|
||||
.send(payload)
|
||||
.expect(200)
|
||||
.expect(200, result);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it(`Start kafka client consumer while waiting for message pattern response.`, async () => {
|
||||
await Promise.all(
|
||||
servers.map(async (server, index) => {
|
||||
// shut off and delete the leader
|
||||
if (index === 0) {
|
||||
return new Promise<void>(resolve => {
|
||||
// wait a second before closing so the producers can send the message to the server consumers
|
||||
setTimeout(async () => {
|
||||
// get the controller
|
||||
const controller = apps[index].get(KafkaConcurrentController);
|
||||
|
||||
// connect the controller client
|
||||
await controller.client.connect();
|
||||
|
||||
// notify the servers that we have started
|
||||
await Promise.all(
|
||||
servers.map(async server => {
|
||||
// send to all servers since indexes don't necessarily align with server consumers
|
||||
return request(server).post('/go').send();
|
||||
}),
|
||||
);
|
||||
|
||||
return resolve();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// send requests
|
||||
const payload = {
|
||||
key: index,
|
||||
numbers: [1, index],
|
||||
};
|
||||
const result = (1 + index).toString();
|
||||
|
||||
return request(server)
|
||||
.post('/mathSumSyncNumberWait')
|
||||
.send(payload)
|
||||
.expect(200)
|
||||
.expect(200, result);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
after(`Stopping Kafka app`, async () => {
|
||||
// close all concurrently
|
||||
return Promise.all(
|
||||
apps.map(async app => {
|
||||
return app.close();
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,10 +9,13 @@ import { UserEntity } from '../src/kafka/entities/user.entity';
|
||||
import { KafkaController } from '../src/kafka/kafka.controller';
|
||||
import { KafkaMessagesController } from '../src/kafka/kafka.messages.controller';
|
||||
|
||||
describe('Kafka transport', () => {
|
||||
describe('Kafka transport', function () {
|
||||
let server;
|
||||
let app: INestApplication;
|
||||
|
||||
// set timeout to be longer (especially for the after hook)
|
||||
this.timeout(30000);
|
||||
|
||||
it(`Start Kafka app`, async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [KafkaController, KafkaMessagesController],
|
||||
@@ -29,6 +32,7 @@ describe('Kafka transport', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
app.enableShutdownHooks();
|
||||
await app.startAllMicroservicesAsync();
|
||||
await app.init();
|
||||
}).timeout(30000);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export class SumDto {
|
||||
key: string;
|
||||
numbers: number[];
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
OnModuleDestroy,
|
||||
OnModuleInit,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { Client, ClientKafka, Transport } from '@nestjs/microservices';
|
||||
import { PartitionerArgs } from 'kafkajs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SumDto } from './dto/sum.dto';
|
||||
|
||||
/**
|
||||
* The following function explicity sends messages to the key representing the partition.
|
||||
*/
|
||||
const explicitPartitioner = () => {
|
||||
return ({ message }: PartitionerArgs) => {
|
||||
return parseFloat(message.headers.toPartition.toString());
|
||||
};
|
||||
};
|
||||
|
||||
@Controller()
|
||||
export class KafkaConcurrentController
|
||||
implements OnModuleInit, OnModuleDestroy {
|
||||
protected readonly logger = new Logger(KafkaConcurrentController.name);
|
||||
|
||||
@Client({
|
||||
transport: Transport.KAFKA,
|
||||
options: {
|
||||
client: {
|
||||
brokers: ['localhost:9092'],
|
||||
},
|
||||
run: {
|
||||
partitionsConsumedConcurrently: 3,
|
||||
},
|
||||
producer: {
|
||||
createPartitioner: explicitPartitioner,
|
||||
},
|
||||
},
|
||||
})
|
||||
public readonly client: ClientKafka;
|
||||
|
||||
async onModuleInit() {
|
||||
const requestPatterns = ['math.sum.sync.number.wait'];
|
||||
|
||||
requestPatterns.forEach(pattern => {
|
||||
this.client.subscribeToResponseOf(pattern);
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.client.close();
|
||||
}
|
||||
|
||||
@Post('mathSumSyncNumberWait')
|
||||
@HttpCode(200)
|
||||
public mathSumSyncNumberWait(@Body() data: SumDto): Observable<string> {
|
||||
return this.client.send('math.sum.sync.number.wait', {
|
||||
headers: {
|
||||
toPartition: data.key.toString(),
|
||||
},
|
||||
key: data.key.toString(),
|
||||
value: data.numbers,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Controller, HttpCode, Post } from '@nestjs/common';
|
||||
import { MessagePattern } from '@nestjs/microservices';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { first, map, skipWhile } from 'rxjs/operators';
|
||||
|
||||
@Controller()
|
||||
export class KafkaConcurrentMessagesController {
|
||||
public waiting = new BehaviorSubject<boolean>(false);
|
||||
|
||||
@Post('go')
|
||||
@HttpCode(200)
|
||||
async go() {
|
||||
// no longer waiting
|
||||
this.waiting.next(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@MessagePattern('math.sum.sync.number.wait')
|
||||
public mathSumSyncNumberWait(data: any): Observable<number> {
|
||||
// start waiting
|
||||
this.waiting.next(true);
|
||||
|
||||
// find sum
|
||||
const sum = data.value[0] + data.value[1];
|
||||
|
||||
return this.waiting.asObservable().pipe(
|
||||
skipWhile(isWaiting => {
|
||||
return isWaiting;
|
||||
}),
|
||||
map(() => {
|
||||
return sum;
|
||||
}),
|
||||
first(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Body, Controller, HttpCode, OnModuleInit, Post } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
OnModuleInit,
|
||||
Post,
|
||||
OnModuleDestroy,
|
||||
} from '@nestjs/common';
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { Client, ClientKafka, Transport } from '@nestjs/microservices';
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -6,7 +13,7 @@ import { BusinessDto } from './dtos/business.dto';
|
||||
import { UserDto } from './dtos/user.dto';
|
||||
|
||||
@Controller()
|
||||
export class KafkaController implements OnModuleInit {
|
||||
export class KafkaController implements OnModuleInit, OnModuleDestroy {
|
||||
protected readonly logger = new Logger(KafkaController.name);
|
||||
static IS_NOTIFIED = false;
|
||||
static MATH_SUM = 0;
|
||||
@@ -21,7 +28,7 @@ export class KafkaController implements OnModuleInit {
|
||||
})
|
||||
private readonly client: ClientKafka;
|
||||
|
||||
onModuleInit() {
|
||||
async onModuleInit() {
|
||||
const requestPatterns = [
|
||||
'math.sum.sync.kafka.message',
|
||||
'math.sum.sync.without.key',
|
||||
@@ -36,6 +43,12 @@ export class KafkaController implements OnModuleInit {
|
||||
requestPatterns.forEach(pattern => {
|
||||
this.client.subscribeToResponseOf(pattern);
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.client.close();
|
||||
}
|
||||
|
||||
// sync send kafka message
|
||||
|
||||
38
integration/nest-application/app-locals/e2e/express.spec.ts
Normal file
38
integration/nest-application/app-locals/e2e/express.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { expect } from 'chai';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('App-level globals (Express Application)', () => {
|
||||
let moduleFixture: TestingModule;
|
||||
let app: NestExpressApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
moduleFixture = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
app = moduleFixture.createNestApplication<NestExpressApplication>();
|
||||
});
|
||||
|
||||
it('should get "title" from "app.locals"', async () => {
|
||||
app.setLocal('title', 'My Website');
|
||||
await app.init();
|
||||
const response = await request(app.getHttpServer()).get('/').expect(200);
|
||||
expect(response.body.title).to.equal('My Website');
|
||||
});
|
||||
|
||||
it('should get "email" from "app.locals"', async () => {
|
||||
app.setLocal('email', 'admin@example.com');
|
||||
await app.listen(4444);
|
||||
const response = await request(app.getHttpServer()).get('/').expect(200);
|
||||
expect(response.body.email).to.equal('admin@example.com');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Controller, Get, Req } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@Get()
|
||||
getGlobals(@Req() req: Request) {
|
||||
return req.app.locals;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule {}
|
||||
22
integration/nest-application/app-locals/tsconfig.json
Normal file
22
integration/nest-application/app-locals/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": false,
|
||||
"noImplicitAny": false,
|
||||
"removeComments": true,
|
||||
"noLib": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"e2e/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -37,6 +37,13 @@ describe('Get URL (Express Application)', () => {
|
||||
expect(await app.getUrl()).to.be.eql(`http://127.0.0.1:${port}`);
|
||||
await app.close();
|
||||
});
|
||||
it('should return 127.0.0.1 even in a callback', () => {
|
||||
const app = testModule.createNestApplication(new ExpressAdapter(express()));
|
||||
return app.listen(port, async () => {
|
||||
expect(await app.getUrl()).to.be.eql(`http://127.0.0.1:${port}`);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
it('should throw an error for calling getUrl before listen', async () => {
|
||||
const app = testModule.createNestApplication(new ExpressAdapter(express()));
|
||||
try {
|
||||
|
||||
@@ -30,6 +30,13 @@ describe('Get URL (Fastify Application)', () => {
|
||||
expect(await app.getUrl()).to.be.eql(`http://127.0.0.1:${port}`);
|
||||
await app.close();
|
||||
});
|
||||
it('should return 127.0.0.1 even in a callback', () => {
|
||||
const app = testModule.createNestApplication(new FastifyAdapter());
|
||||
return app.listen(port, async () => {
|
||||
expect(await app.getUrl()).to.be.eql(`http://127.0.0.1:${port}`);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
it('should throw an error for calling getUrl before listen', async () => {
|
||||
const app = testModule.createNestApplication(new FastifyAdapter());
|
||||
try {
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('ErrorGateway', () => {
|
||||
ws.emit('push', {
|
||||
test: 'test',
|
||||
});
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('exception', data => {
|
||||
expect(data).to.be.eql({
|
||||
status: 'error',
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('WebSocketGateway (ack)', () => {
|
||||
await app.listenAsync(3000);
|
||||
|
||||
ws = io.connect('http://localhost:8080');
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.emit('push', { test: 'test' }, data => {
|
||||
expect(data).to.be.eql('pong');
|
||||
resolve();
|
||||
@@ -33,7 +33,7 @@ describe('WebSocketGateway (ack)', () => {
|
||||
await app.listenAsync(3000);
|
||||
|
||||
ws = io.connect('http://localhost:8080');
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.emit('push', data => {
|
||||
expect(data).to.be.eql('pong');
|
||||
resolve();
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('WebSocketGateway', () => {
|
||||
ws.emit('push', {
|
||||
test: 'test',
|
||||
});
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('pop', data => {
|
||||
expect(data.test).to.be.eql('test');
|
||||
resolve();
|
||||
@@ -41,7 +41,7 @@ describe('WebSocketGateway', () => {
|
||||
ws.emit('push', {
|
||||
test: 'test',
|
||||
});
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('pop', data => {
|
||||
expect(data.test).to.be.eql('test');
|
||||
resolve();
|
||||
@@ -58,7 +58,7 @@ describe('WebSocketGateway', () => {
|
||||
ws.emit('push', {
|
||||
test: 'test',
|
||||
});
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('pop', data => {
|
||||
expect(data.test).to.be.eql('test');
|
||||
resolve();
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('WebSocketGateway (WsAdapter)', () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('message', data => {
|
||||
expect(JSON.parse(data).data.test).to.be.eql('test');
|
||||
resolve();
|
||||
@@ -57,7 +57,7 @@ describe('WebSocketGateway (WsAdapter)', () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('message', data => {
|
||||
expect(JSON.parse(data).data.test).to.be.eql('test');
|
||||
resolve();
|
||||
@@ -77,7 +77,7 @@ describe('WebSocketGateway (WsAdapter)', () => {
|
||||
ws = new WebSocket('ws://localhost:8080');
|
||||
ws2 = new WebSocket('ws://localhost:8090');
|
||||
|
||||
await new Promise(resolve =>
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('open', () => {
|
||||
ws.on('message', data => {
|
||||
expect(JSON.parse(data).data.test).to.be.eql('test');
|
||||
@@ -94,7 +94,7 @@ describe('WebSocketGateway (WsAdapter)', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await new Promise(resolve => {
|
||||
await new Promise<void>(resolve => {
|
||||
ws2.on('message', data => {
|
||||
expect(JSON.parse(data).data.test).to.be.eql('test');
|
||||
resolve();
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "7.5.5"
|
||||
"version": "7.6.7"
|
||||
}
|
||||
|
||||
2471
package-lock.json
generated
2471
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
101
package.json
101
package.json
@@ -29,9 +29,10 @@
|
||||
"test:docker:up": "docker-compose -f integration/docker-compose.yml up -d",
|
||||
"test:docker:down": "docker-compose -f integration/docker-compose.yml down",
|
||||
"lint": "concurrently 'npm run lint:packages' 'npm run lint:integration' 'npm run lint:spec'",
|
||||
"lint:integration": "eslint 'integration/*/{,!(node_modules)/**/}/*.ts' -c '.eslintrc.spec.js' --fix",
|
||||
"lint:packages": "eslint 'packages/**/**.ts' --fix --ignore-pattern 'packages/**/*.spec.ts'",
|
||||
"lint:spec": "eslint 'packages/**/**.spec.ts' -c '.eslintrc.spec.js' --fix",
|
||||
"lint:fix": "concurrently 'npm run lint:packages -- --fix' 'npm run lint:integration -- --fix' 'npm run lint:spec -- --fix'",
|
||||
"lint:integration": "eslint 'integration/*/{,!(node_modules)/**/}/*.ts' -c '.eslintrc.spec.js'",
|
||||
"lint:packages": "eslint 'packages/**/**.ts' --ignore-pattern 'packages/**/*.spec.ts'",
|
||||
"lint:spec": "eslint 'packages/**/**.spec.ts' -c '.eslintrc.spec.js'",
|
||||
"prerelease": "gulp copy-misc && gulp build --dist node_modules/@nestjs",
|
||||
"publish": "npm run prerelease && npm run build:prod && ./node_modules/.bin/lerna publish --force-publish --access public --exact -m \"chore(@nestjs) publish %s release\"",
|
||||
"publish:beta": "npm run prerelease && npm run build:prod && ./node_modules/.bin/lerna publish --npm-tag=beta --access public -m \"chore(@nestjs) publish %s release\"",
|
||||
@@ -52,52 +53,52 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/opencollective": "0.3.2",
|
||||
"axios": "0.21.0",
|
||||
"class-transformer": "0.3.1",
|
||||
"class-validator": "0.12.2",
|
||||
"axios": "0.21.1",
|
||||
"class-transformer": "0.3.2",
|
||||
"class-validator": "0.13.1",
|
||||
"cli-color": "2.0.0",
|
||||
"cors": "2.8.5",
|
||||
"express": "4.17.1",
|
||||
"fast-json-stringify": "2.2.10",
|
||||
"fast-json-stringify": "2.4.1",
|
||||
"fast-safe-stringify": "2.0.7",
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "2.0.3",
|
||||
"object-hash": "2.1.1",
|
||||
"path-to-regexp": "3.2.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "6.6.3",
|
||||
"socket.io": "2.3.0",
|
||||
"uuid": "8.3.1",
|
||||
"tslib": "2.0.3"
|
||||
"socket.io": "2.4.1",
|
||||
"tslib": "2.1.0",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codechecks/client": "0.1.10",
|
||||
"@commitlint/cli": "11.0.0",
|
||||
"@commitlint/config-angular": "11.0.0",
|
||||
"@grpc/proto-loader": "0.5.5",
|
||||
"@nestjs/graphql": "7.9.1",
|
||||
"@nestjs/mongoose": "7.1.2",
|
||||
"@grpc/proto-loader": "0.5.6",
|
||||
"@nestjs/graphql": "7.9.8",
|
||||
"@nestjs/mongoose": "7.2.2",
|
||||
"@nestjs/typeorm": "7.1.5",
|
||||
"@types/amqplib": "0.5.16",
|
||||
"@types/amqplib": "0.5.17",
|
||||
"@types/bytes": "3.1.0",
|
||||
"@types/cache-manager": "2.10.3",
|
||||
"@types/cache-manager": "3.4.0",
|
||||
"@types/chai": "4.2.14",
|
||||
"@types/chai-as-promised": "7.1.3",
|
||||
"@types/cors": "2.8.8",
|
||||
"@types/express": "4.17.9",
|
||||
"@types/gulp": "4.0.7",
|
||||
"@types/mocha": "8.0.4",
|
||||
"@types/mongoose": "5.10.1",
|
||||
"@types/node": "14.14.10",
|
||||
"@types/cors": "2.8.9",
|
||||
"@types/express": "4.17.11",
|
||||
"@types/gulp": "4.0.8",
|
||||
"@types/mocha": "8.2.0",
|
||||
"@types/mongoose": "5.10.3",
|
||||
"@types/node": "14.14.22",
|
||||
"@types/redis": "2.8.28",
|
||||
"@types/reflect-metadata": "0.1.0",
|
||||
"@types/sinon": "9.0.9",
|
||||
"@types/socket.io": "2.1.11",
|
||||
"@types/sinon": "9.0.10",
|
||||
"@types/socket.io": "2.1.13",
|
||||
"@types/ws": "7.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.9.0",
|
||||
"@typescript-eslint/parser": "4.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.14.1",
|
||||
"@typescript-eslint/parser": "4.14.1",
|
||||
"amqp-connection-manager": "3.2.1",
|
||||
"amqplib": "0.6.0",
|
||||
"apollo-server-express": "2.19.0",
|
||||
"apollo-server-express": "2.19.2",
|
||||
"artillery": "1.6.1",
|
||||
"awesome-typescript-loader": "5.2.1",
|
||||
"body-parser": "1.19.0",
|
||||
@@ -105,25 +106,25 @@
|
||||
"cache-manager": "3.4.0",
|
||||
"chai": "4.2.0",
|
||||
"chai-as-promised": "7.1.1",
|
||||
"clang-format": "1.4.0",
|
||||
"clang-format": "1.5.0",
|
||||
"commitlint-circle": "1.0.0",
|
||||
"concurrently": "5.3.0",
|
||||
"conventional-changelog": "3.1.24",
|
||||
"core-js": "3.8.0",
|
||||
"core-js": "3.8.3",
|
||||
"coveralls": "3.1.0",
|
||||
"delete-empty": "3.0.0",
|
||||
"engine.io-client": "4.0.4",
|
||||
"eslint": "7.14.0",
|
||||
"eslint-config-prettier": "6.15.0",
|
||||
"engine.io-client": "4.1.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-prettier": "7.2.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eventsource": "1.0.7",
|
||||
"fancy-log": "1.3.3",
|
||||
"fastify": "3.9.1",
|
||||
"fastify-cors": "5.0.0",
|
||||
"fastify": "3.11.0",
|
||||
"fastify-cors": "5.2.0",
|
||||
"fastify-formbody": "5.0.0",
|
||||
"fastify-multipart": "3.3.1",
|
||||
"fastify-static": "3.3.0",
|
||||
"graphql": "15.4.0",
|
||||
"fastify-static": "3.4.0",
|
||||
"graphql": "15.5.0",
|
||||
"graphql-tools": "7.0.2",
|
||||
"grpc": "1.24.4",
|
||||
"gulp": "4.0.2",
|
||||
@@ -132,39 +133,39 @@
|
||||
"gulp-sourcemaps": "3.0.0",
|
||||
"gulp-typescript": "5.0.1",
|
||||
"gulp-watch": "5.0.1",
|
||||
"husky": "4.3.0",
|
||||
"husky": "4.3.8",
|
||||
"imports-loader": "1.2.0",
|
||||
"json-loader": "0.5.7",
|
||||
"kafkajs": "1.12.0",
|
||||
"kafkajs": "1.15.0",
|
||||
"lerna": "2.11.0",
|
||||
"light-my-request": "4.3.0",
|
||||
"lint-staged": "10.5.2",
|
||||
"light-my-request": "4.4.1",
|
||||
"lint-staged": "10.5.3",
|
||||
"markdown-table": "2.0.0",
|
||||
"merge-graphql-schemas": "1.7.8",
|
||||
"middie": "5.2.0",
|
||||
"mocha": "8.2.1",
|
||||
"mongoose": "5.11.2",
|
||||
"mongoose": "5.11.13",
|
||||
"mqtt": "4.2.6",
|
||||
"multer": "1.4.2",
|
||||
"mysql": "2.18.1",
|
||||
"nats": "1.4.12",
|
||||
"nodemon": "2.0.6",
|
||||
"nodemon": "2.0.7",
|
||||
"nyc": "15.1.0",
|
||||
"point-of-view": "4.7.0",
|
||||
"point-of-view": "4.9.0",
|
||||
"prettier": "2.2.1",
|
||||
"redis": "3.0.2",
|
||||
"rxjs-compat": "6.6.3",
|
||||
"sinon": "9.2.1",
|
||||
"sinon": "9.2.4",
|
||||
"sinon-chai": "3.5.0",
|
||||
"socket.io-client": "2.3.1",
|
||||
"socket.io-client": "2.4.0",
|
||||
"subscriptions-transport-ws": "0.9.18",
|
||||
"supertest": "6.0.1",
|
||||
"supertest": "6.1.3",
|
||||
"ts-morph": "9.1.0",
|
||||
"ts-node": "9.0.0",
|
||||
"typeorm": "0.2.29",
|
||||
"typescript": "4.0.3",
|
||||
"ts-node": "9.1.1",
|
||||
"typeorm": "0.2.30",
|
||||
"typescript": "4.1.3",
|
||||
"wrk": "1.2.1",
|
||||
"ws": "7.4.0"
|
||||
"ws": "7.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
PATH_METADATA,
|
||||
SCOPE_OPTIONS_METADATA,
|
||||
} from '../../constants';
|
||||
import { isString, isUndefined } from '../../utils/shared.utils';
|
||||
import { ScopeOptions } from '../../interfaces/scope-options.interface';
|
||||
import { isString, isUndefined } from '../../utils/shared.utils';
|
||||
|
||||
/**
|
||||
* Interface defining options that can be passed to `@Controller()` decorator
|
||||
@@ -18,7 +18,7 @@ export interface ControllerOptions extends ScopeOptions {
|
||||
*
|
||||
* @see [Routing](https://docs.nestjs.com/controllers#routing)
|
||||
*/
|
||||
path?: string;
|
||||
path?: string | string[];
|
||||
|
||||
/**
|
||||
* Specifies an optional HTTP Request host filter. When configured, methods
|
||||
@@ -65,7 +65,7 @@ export function Controller(): ClassDecorator;
|
||||
* It defines a class that provides a context for one or more message or event
|
||||
* handlers.
|
||||
*
|
||||
* @param {string} prefix string that defines a `route path prefix`. The prefix
|
||||
* @param {string, Array} prefix string that defines a `route path prefix`. The prefix
|
||||
* is pre-pended to the path specified in any request decorator in the class.
|
||||
*
|
||||
* @see [Routing](https://docs.nestjs.com/controllers#routing)
|
||||
@@ -74,7 +74,7 @@ export function Controller(): ClassDecorator;
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export function Controller(prefix: string): ClassDecorator;
|
||||
export function Controller(prefix: string | string[]): ClassDecorator;
|
||||
|
||||
/**
|
||||
* Decorator that marks a class as a Nest controller that can receive inbound
|
||||
@@ -137,12 +137,13 @@ export function Controller(options: ControllerOptions): ClassDecorator;
|
||||
* @publicApi
|
||||
*/
|
||||
export function Controller(
|
||||
prefixOrOptions?: string | ControllerOptions,
|
||||
prefixOrOptions?: string | string[] | ControllerOptions,
|
||||
): ClassDecorator {
|
||||
const defaultPath = '/';
|
||||
|
||||
const [path, host, scopeOptions] = isUndefined(prefixOrOptions)
|
||||
? [defaultPath, undefined, undefined]
|
||||
: isString(prefixOrOptions)
|
||||
: isString(prefixOrOptions) || Array.isArray(prefixOrOptions)
|
||||
? [prefixOrOptions, undefined, undefined]
|
||||
: [
|
||||
prefixOrOptions.path || defaultPath,
|
||||
|
||||
@@ -2,6 +2,7 @@ export enum HttpStatus {
|
||||
CONTINUE = 100,
|
||||
SWITCHING_PROTOCOLS = 101,
|
||||
PROCESSING = 102,
|
||||
EARLYHINTS = 103,
|
||||
OK = 200,
|
||||
CREATED = 201,
|
||||
ACCEPTED = 202,
|
||||
|
||||
@@ -57,6 +57,10 @@ export class HttpException extends Error {
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
public initName(): void {
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
|
||||
public getResponse(): string | object {
|
||||
return this.response;
|
||||
@@ -78,8 +82,4 @@ export class HttpException extends Error {
|
||||
? objectOrError
|
||||
: { statusCode, message: objectOrError, error: description };
|
||||
}
|
||||
|
||||
public initName(): void {
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,3 +52,10 @@ export interface CorsOptions {
|
||||
*/
|
||||
optionsSuccessStatus?: number;
|
||||
}
|
||||
|
||||
export interface CorsOptionsCallback {
|
||||
(error: Error, options: CorsOptions): void;
|
||||
}
|
||||
export interface CorsOptionsDelegate<T> {
|
||||
(req: T, cb: CorsOptionsCallback): void;
|
||||
}
|
||||
|
||||
@@ -2,29 +2,41 @@
|
||||
* Validation error description.
|
||||
* @see https://github.com/typestack/class-validator
|
||||
*
|
||||
* class-validator@0.13.0
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface ValidationError {
|
||||
/**
|
||||
* Object that was validated.
|
||||
*
|
||||
* OPTIONAL - configurable via the ValidatorOptions.validationError.target option
|
||||
*/
|
||||
target: Record<string, any>;
|
||||
target?: Record<string, any>;
|
||||
/**
|
||||
* Object's property that hasn't passed validation.
|
||||
*/
|
||||
property: string;
|
||||
/**
|
||||
* Value that hasn't passed validation.
|
||||
* Value that haven't pass a validation.
|
||||
*
|
||||
* OPTIONAL - configurable via the ValidatorOptions.validationError.value option
|
||||
*/
|
||||
value: any;
|
||||
value?: any;
|
||||
/**
|
||||
* Constraints that failed validation with error messages.
|
||||
*/
|
||||
constraints: {
|
||||
constraints?: {
|
||||
[type: string]: string;
|
||||
};
|
||||
/**
|
||||
* Contains all nested validation errors of the property.
|
||||
*/
|
||||
children: ValidationError[];
|
||||
children?: ValidationError[];
|
||||
/**
|
||||
* A transient set of data passed through to the validation result for response mapping
|
||||
*/
|
||||
contexts?: {
|
||||
[type: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
* Options passed to validator during validation.
|
||||
* @see https://github.com/typestack/class-validator
|
||||
*
|
||||
* class-validator@0.10.1
|
||||
* class-validator@0.13.0
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface ValidatorOptions {
|
||||
/**
|
||||
* If set to true then class-validator will print extra warning messages to the console when something is not right.
|
||||
*/
|
||||
enableDebugMessages?: boolean;
|
||||
/**
|
||||
* If set to true then validator will skip validation of all properties that are undefined in the validating object.
|
||||
*/
|
||||
@@ -33,6 +37,15 @@ export interface ValidatorOptions {
|
||||
* Groups to be used during validation of the object.
|
||||
*/
|
||||
groups?: string[];
|
||||
/**
|
||||
* Set default for `always` option of decorators. Default can be overridden in decorator options.
|
||||
*/
|
||||
always?: boolean;
|
||||
/**
|
||||
* If [groups]{@link ValidatorOptions#groups} is not given or is empty,
|
||||
* ignore decorators with at least one group.
|
||||
*/
|
||||
strictGroups?: boolean;
|
||||
/**
|
||||
* If set to true, the validation will not use default messages.
|
||||
* Error message always will be undefined if its not explicitly set.
|
||||
@@ -55,4 +68,8 @@ export interface ValidatorOptions {
|
||||
* Settings true will cause fail validation of unknown objects.
|
||||
*/
|
||||
forbidUnknownValues?: boolean;
|
||||
/**
|
||||
* When set to true, validation of the given property will stop after encountering the first error. Defaults to false.
|
||||
*/
|
||||
stopAtFirstError?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { RequestMethod } from '../../enums';
|
||||
import { CorsOptions } from '../../interfaces/external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from '../../interfaces/external/cors-options.interface';
|
||||
import { NestApplicationOptions } from '../../interfaces/nest-application-options.interface';
|
||||
|
||||
export type ErrorHandler<TRequest = any, TResponse = any> = (
|
||||
@@ -62,7 +65,7 @@ export interface HttpServer<TRequest = any, TResponse = any> {
|
||||
getRequestUrl?(request: TResponse): string;
|
||||
getInstance(): any;
|
||||
registerParserMiddleware(): any;
|
||||
enableCors(options: CorsOptions): any;
|
||||
enableCors(options: CorsOptions | CorsOptionsDelegate<TRequest>): any;
|
||||
getHttpServer(): any;
|
||||
initHttpServer(options: NestApplicationOptions): void;
|
||||
close(): any;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ShutdownSignal } from '../enums/shutdown-signal.enum';
|
||||
import { LoggerService, LogLevel } from '../services/logger.service';
|
||||
import { Abstract } from './abstract.interface';
|
||||
import { DynamicModule } from './modules';
|
||||
import { Type } from './type.interface';
|
||||
|
||||
/**
|
||||
@@ -13,7 +14,7 @@ export interface INestApplicationContext {
|
||||
* Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module.
|
||||
* @returns {INestApplicationContext}
|
||||
*/
|
||||
select<T>(module: Type<T>): INestApplicationContext;
|
||||
select<T>(module: Type<T> | DynamicModule): INestApplicationContext;
|
||||
|
||||
/**
|
||||
* Retrieves an instance of either injectable or controller, otherwise, throws exception.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { CorsOptions } from './external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from './external/cors-options.interface';
|
||||
import { HttpsOptions } from './external/https-options.interface';
|
||||
import { NestApplicationContextOptions } from './nest-application-context-options.interface';
|
||||
|
||||
@@ -9,7 +12,7 @@ export interface NestApplicationOptions extends NestApplicationContextOptions {
|
||||
/**
|
||||
* CORS options from [CORS package](https://github.com/expressjs/cors#configuration-options)
|
||||
*/
|
||||
cors?: boolean | CorsOptions;
|
||||
cors?: boolean | CorsOptions | CorsOptionsDelegate<any>;
|
||||
/**
|
||||
* Whether to use underlying platform body parser.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { CorsOptions } from './external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from './external/cors-options.interface';
|
||||
import { CanActivate } from './features/can-activate.interface';
|
||||
import { NestInterceptor } from './features/nest-interceptor.interface';
|
||||
import { HttpServer } from './http/http-server.interface';
|
||||
@@ -30,7 +33,7 @@ export interface INestApplication extends INestApplicationContext {
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
enableCors(options?: CorsOptions): void;
|
||||
enableCors(options?: CorsOptions | CorsOptionsDelegate<any>): void;
|
||||
|
||||
/**
|
||||
* Starts the application.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface Type<T> extends Function {
|
||||
export interface Type<T = any> extends Function {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/common",
|
||||
"version": "7.5.5",
|
||||
"version": "7.6.7",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"homepage": "https://nestjs.com",
|
||||
@@ -17,13 +17,27 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "0.21.0",
|
||||
"axios": "0.21.1",
|
||||
"iterare": "1.2.1",
|
||||
"tslib": "2.0.3",
|
||||
"uuid": "8.3.1"
|
||||
"tslib": "2.1.0",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cache-manager": "*",
|
||||
"class-transformer": "*",
|
||||
"class-validator": "*",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"cache-manager": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
},
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,27 +199,35 @@ export class ValidationPipe implements PipeTransform<any> {
|
||||
|
||||
protected mapChildrenToValidationErrors(
|
||||
error: ValidationError,
|
||||
parentPath?: string,
|
||||
): ValidationError[] {
|
||||
if (!(error.children && error.children.length)) {
|
||||
return [error];
|
||||
}
|
||||
const validationErrors = [];
|
||||
parentPath = parentPath
|
||||
? `${parentPath}.${error.property}`
|
||||
: error.property;
|
||||
for (const item of error.children) {
|
||||
if (item.children && item.children.length) {
|
||||
validationErrors.push(...this.mapChildrenToValidationErrors(item));
|
||||
validationErrors.push(
|
||||
...this.mapChildrenToValidationErrors(item, parentPath),
|
||||
);
|
||||
}
|
||||
validationErrors.push(this.prependConstraintsWithParentProp(error, item));
|
||||
validationErrors.push(
|
||||
this.prependConstraintsWithParentProp(parentPath, item),
|
||||
);
|
||||
}
|
||||
return validationErrors;
|
||||
}
|
||||
|
||||
protected prependConstraintsWithParentProp(
|
||||
parentError: ValidationError,
|
||||
parentPath: string,
|
||||
error: ValidationError,
|
||||
): ValidationError {
|
||||
const constraints = {};
|
||||
for (const key in error.constraints) {
|
||||
constraints[key] = `${parentError.property}.${error.constraints[key]}`;
|
||||
constraints[key] = `${parentPath}.${error.constraints[key]}`;
|
||||
}
|
||||
return {
|
||||
...error,
|
||||
|
||||
@@ -71,7 +71,7 @@ export class ClassSerializerInterceptor implements NestInterceptor {
|
||||
: plainOrClass;
|
||||
}
|
||||
|
||||
private getContextOptions(
|
||||
protected getContextOptions(
|
||||
context: ExecutionContext,
|
||||
): ClassTransformOptions | undefined {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '../decorators/core/injectable.decorator';
|
||||
import { Optional } from '../decorators/core/optional.decorator';
|
||||
import { clc, yellow } from '../utils/cli-colors.util';
|
||||
import { isObject } from '../utils/shared.utils';
|
||||
import { isObject, isPlainObject } from '../utils/shared.utils';
|
||||
|
||||
declare const process: any;
|
||||
|
||||
@@ -25,7 +25,7 @@ export class Logger implements LoggerService {
|
||||
'verbose',
|
||||
];
|
||||
private static lastTimestamp?: number;
|
||||
private static instance?: typeof Logger | LoggerService = Logger;
|
||||
protected static instance?: typeof Logger | LoggerService = Logger;
|
||||
|
||||
constructor(
|
||||
@Optional() protected context?: string,
|
||||
@@ -61,6 +61,10 @@ export class Logger implements LoggerService {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
getTimestamp() {
|
||||
return Logger.getTimestamp();
|
||||
}
|
||||
|
||||
static overrideLogger(logger: LoggerService | LogLevel[] | boolean) {
|
||||
if (Array.isArray(logger)) {
|
||||
this.logLevels = logger;
|
||||
@@ -79,7 +83,7 @@ export class Logger implements LoggerService {
|
||||
context = '',
|
||||
isTimeDiffEnabled = true,
|
||||
) {
|
||||
this.printMessage(message, clc.red, context, isTimeDiffEnabled);
|
||||
this.printMessage(message, clc.red, context, isTimeDiffEnabled, 'stderr');
|
||||
this.printStackTrace(trace);
|
||||
}
|
||||
|
||||
@@ -95,6 +99,18 @@ export class Logger implements LoggerService {
|
||||
this.printMessage(message, clc.cyanBright, context, isTimeDiffEnabled);
|
||||
}
|
||||
|
||||
static getTimestamp() {
|
||||
const localeStringOptions = {
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
};
|
||||
return new Date(Date.now()).toLocaleString(undefined, localeStringOptions);
|
||||
}
|
||||
|
||||
private callFunction(
|
||||
name: 'log' | 'warn' | 'debug' | 'verbose',
|
||||
message: any,
|
||||
@@ -114,7 +130,7 @@ export class Logger implements LoggerService {
|
||||
);
|
||||
}
|
||||
|
||||
private getInstance(): typeof Logger | LoggerService {
|
||||
protected getInstance(): typeof Logger | LoggerService {
|
||||
const { instance } = Logger;
|
||||
return instance === this ? Logger : instance;
|
||||
}
|
||||
@@ -128,31 +144,22 @@ export class Logger implements LoggerService {
|
||||
color: (message: string) => string,
|
||||
context = '',
|
||||
isTimeDiffEnabled?: boolean,
|
||||
writeStreamType?: 'stdout' | 'stderr',
|
||||
) {
|
||||
const output = isObject(message)
|
||||
const output = isPlainObject(message)
|
||||
? `${color('Object:')}\n${JSON.stringify(message, null, 2)}\n`
|
||||
: color(message);
|
||||
|
||||
const localeStringOptions = {
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
};
|
||||
const timestamp = new Date(Date.now()).toLocaleString(
|
||||
undefined,
|
||||
localeStringOptions,
|
||||
);
|
||||
|
||||
const pidMessage = color(`[Nest] ${process.pid} - `);
|
||||
const contextMessage = context ? yellow(`[${context}] `) : '';
|
||||
const timestampDiff = this.updateAndGetTimestampDiff(isTimeDiffEnabled);
|
||||
const instance = (this.instance as typeof Logger) ?? Logger;
|
||||
const timestamp = instance.getTimestamp
|
||||
? instance.getTimestamp()
|
||||
: Logger.getTimestamp?.();
|
||||
const computedMessage = `${pidMessage}${timestamp} ${contextMessage}${output}${timestampDiff}\n`;
|
||||
|
||||
process.stdout.write(
|
||||
`${pidMessage}${timestamp} ${contextMessage}${output}${timestampDiff}\n`,
|
||||
);
|
||||
process[writeStreamType ?? 'stdout'].write(computedMessage);
|
||||
}
|
||||
|
||||
private static updateAndGetTimestampDiff(
|
||||
@@ -170,6 +177,6 @@ export class Logger implements LoggerService {
|
||||
if (!trace) {
|
||||
return;
|
||||
}
|
||||
process.stdout.write(`${trace}\n`);
|
||||
process.stderr.write(`${trace}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
IsArray,
|
||||
} from 'class-validator';
|
||||
import { HttpStatus } from '../../enums';
|
||||
import { UnprocessableEntityException } from '../../exceptions';
|
||||
@@ -156,6 +157,32 @@ describe('ValidationPipe', () => {
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
class TestModelForNestedArrayValidation {
|
||||
@IsString()
|
||||
public prop: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested()
|
||||
@Type(() => TestModel2)
|
||||
public test: TestModel2[];
|
||||
}
|
||||
it('should provide complete path for nested errors', async () => {
|
||||
try {
|
||||
const model = new TestModelForNestedArrayValidation();
|
||||
model.test = [new TestModel2()];
|
||||
await target.transform(model, {
|
||||
type: 'body',
|
||||
metatype: TestModelForNestedArrayValidation,
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err.getResponse().message).to.be.eql([
|
||||
'prop must be a string',
|
||||
'test.0.prop1 must be a string',
|
||||
'test.0.prop2 must be a boolean value',
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('when validation transforms', () => {
|
||||
it('should return a TestModel instance', async () => {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
isUndefined,
|
||||
isFunction,
|
||||
isObject,
|
||||
isString,
|
||||
addLeadingSlash,
|
||||
isConstructor,
|
||||
validatePath,
|
||||
isNil,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
isNil,
|
||||
isObject,
|
||||
isPlainObject,
|
||||
isString,
|
||||
isUndefined,
|
||||
} from '../../utils/shared.utils';
|
||||
|
||||
function Foo(a) {
|
||||
@@ -81,17 +81,17 @@ describe('Shared utils', () => {
|
||||
expect(isConstructor('nope')).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('validatePath', () => {
|
||||
describe('addLeadingSlash', () => {
|
||||
it('should returns validated path ("add / if not exists")', () => {
|
||||
expect(validatePath('nope')).to.be.eql('/nope');
|
||||
expect(addLeadingSlash('nope')).to.be.eql('/nope');
|
||||
});
|
||||
it('should returns same path', () => {
|
||||
expect(validatePath('/nope')).to.be.eql('/nope');
|
||||
expect(addLeadingSlash('/nope')).to.be.eql('/nope');
|
||||
});
|
||||
it('should returns empty path', () => {
|
||||
expect(validatePath('')).to.be.eql('');
|
||||
expect(validatePath(null)).to.be.eql('');
|
||||
expect(validatePath(undefined)).to.be.eql('');
|
||||
expect(addLeadingSlash('')).to.be.eql('');
|
||||
expect(addLeadingSlash(null)).to.be.eql('');
|
||||
expect(addLeadingSlash(undefined)).to.be.eql('');
|
||||
});
|
||||
});
|
||||
describe('isNil', () => {
|
||||
|
||||
@@ -24,9 +24,15 @@ export const isPlainObject = (fn: any): fn is object => {
|
||||
);
|
||||
};
|
||||
|
||||
export const validatePath = (path?: string): string =>
|
||||
export const addLeadingSlash = (path?: string): string =>
|
||||
path ? (path.charAt(0) !== '/' ? '/' + path : path) : '';
|
||||
|
||||
/**
|
||||
* Deprecated. Use the "addLeadingSlash" function instead.
|
||||
* @deprecated
|
||||
*/
|
||||
export const validatePath = addLeadingSlash;
|
||||
|
||||
export const isFunction = (fn: any): boolean => typeof fn === 'function';
|
||||
export const isString = (fn: any): fn is string => typeof fn === 'string';
|
||||
export const isConstructor = (fn: any): boolean => fn === 'constructor';
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { HttpServer, RequestMethod } from '@nestjs/common';
|
||||
import { RequestHandler } from '@nestjs/common/interfaces';
|
||||
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';
|
||||
|
||||
/**
|
||||
@@ -14,6 +17,14 @@ export abstract class AbstractHttpAdapter<
|
||||
protected httpServer: TServer;
|
||||
|
||||
constructor(protected readonly instance: any) {}
|
||||
all(path: string, handler: RequestHandler<TRequest, TResponse>);
|
||||
all(handler: RequestHandler<TRequest, TResponse>);
|
||||
all(path: any, handler?: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
setBaseViewsDir?(path: string | string[]): this {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
public async init() {}
|
||||
@@ -97,7 +108,10 @@ export abstract class AbstractHttpAdapter<
|
||||
abstract setNotFoundHandler(handler: Function, prefix?: string);
|
||||
abstract setHeader(response, name: string, value: string);
|
||||
abstract registerParserMiddleware(prefix?: string);
|
||||
abstract enableCors(options: CorsOptions, prefix?: string);
|
||||
abstract enableCors(
|
||||
options: CorsOptions | CorsOptionsDelegate<TRequest>,
|
||||
prefix?: string,
|
||||
);
|
||||
abstract createMiddlewareFactory(
|
||||
requestMethod: RequestMethod,
|
||||
):
|
||||
|
||||
@@ -14,12 +14,8 @@ export class ApplicationConfig {
|
||||
private globalInterceptors: NestInterceptor[] = [];
|
||||
private globalGuards: CanActivate[] = [];
|
||||
private readonly globalRequestPipes: InstanceWrapper<PipeTransform>[] = [];
|
||||
private readonly globalRequestFilters: InstanceWrapper<
|
||||
ExceptionFilter
|
||||
>[] = [];
|
||||
private readonly globalRequestInterceptors: InstanceWrapper<
|
||||
NestInterceptor
|
||||
>[] = [];
|
||||
private readonly globalRequestFilters: InstanceWrapper<ExceptionFilter>[] = [];
|
||||
private readonly globalRequestInterceptors: InstanceWrapper<NestInterceptor>[] = [];
|
||||
private readonly globalRequestGuards: InstanceWrapper<CanActivate>[] = [];
|
||||
|
||||
constructor(private ioAdapter: WebSocketAdapter | null = null) {}
|
||||
|
||||
@@ -38,9 +38,7 @@ export interface ExternalContextOptions {
|
||||
export class ExternalContextCreator {
|
||||
private readonly contextUtils = new ContextUtils();
|
||||
private readonly externalErrorProxy = new ExternalErrorProxy();
|
||||
private readonly handlerMetadataStorage = new HandlerMetadataStorage<
|
||||
ExternalHandlerMetadata
|
||||
>();
|
||||
private readonly handlerMetadataStorage = new HandlerMetadataStorage<ExternalHandlerMetadata>();
|
||||
private container: NestContainer;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -13,18 +13,17 @@ export class ModuleCompiler {
|
||||
public async compile(
|
||||
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
|
||||
): Promise<ModuleFactory> {
|
||||
const { type, dynamicMetadata } = await this.extractMetadata(metatype);
|
||||
const { type, dynamicMetadata } = this.extractMetadata(await metatype);
|
||||
const token = this.moduleTokenFactory.create(type, dynamicMetadata);
|
||||
return { type, dynamicMetadata, token };
|
||||
}
|
||||
|
||||
public async extractMetadata(
|
||||
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
|
||||
): Promise<{
|
||||
public extractMetadata(
|
||||
metatype: Type<any> | DynamicModule,
|
||||
): {
|
||||
type: Type<any>;
|
||||
dynamicMetadata?: Partial<DynamicModule> | undefined;
|
||||
}> {
|
||||
metatype = await metatype;
|
||||
} {
|
||||
if (!this.isDynamicModule(metatype)) {
|
||||
return { type: metatype };
|
||||
}
|
||||
|
||||
@@ -69,82 +69,6 @@ export interface InjectorDependencyContext {
|
||||
}
|
||||
|
||||
export class Injector {
|
||||
public async loadMiddleware(
|
||||
wrapper: InstanceWrapper,
|
||||
collection: Map<string, InstanceWrapper>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const { metatype } = wrapper;
|
||||
const targetWrapper = collection.get(metatype.name);
|
||||
if (!isUndefined(targetWrapper.instance)) {
|
||||
return;
|
||||
}
|
||||
const loadInstance = (instances: any[]) => {
|
||||
targetWrapper.instance = targetWrapper.isDependencyTreeStatic()
|
||||
? new (metatype as Type<any>)(...instances)
|
||||
: Object.create(metatype.prototype);
|
||||
};
|
||||
await this.resolveConstructorParams(
|
||||
wrapper,
|
||||
moduleRef,
|
||||
null,
|
||||
loadInstance,
|
||||
contextId,
|
||||
inquirer,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadController(
|
||||
wrapper: InstanceWrapper<Controller>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
) {
|
||||
const controllers = moduleRef.controllers;
|
||||
await this.loadInstance<Controller>(
|
||||
wrapper,
|
||||
controllers,
|
||||
moduleRef,
|
||||
contextId,
|
||||
wrapper,
|
||||
);
|
||||
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
|
||||
}
|
||||
|
||||
public async loadInjectable<T = any>(
|
||||
wrapper: InstanceWrapper<T>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const injectables = moduleRef.injectables;
|
||||
await this.loadInstance<T>(
|
||||
wrapper,
|
||||
injectables,
|
||||
moduleRef,
|
||||
contextId,
|
||||
inquirer,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadProvider(
|
||||
wrapper: InstanceWrapper<Injectable>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const providers = moduleRef.providers;
|
||||
await this.loadInstance<Injectable>(
|
||||
wrapper,
|
||||
providers,
|
||||
moduleRef,
|
||||
contextId,
|
||||
inquirer,
|
||||
);
|
||||
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
|
||||
}
|
||||
|
||||
public loadPrototype<T>(
|
||||
{ name }: InstanceWrapper<T>,
|
||||
collection: Map<string, InstanceWrapper<T>>,
|
||||
@@ -164,15 +88,6 @@ export class Injector {
|
||||
}
|
||||
}
|
||||
|
||||
public applyDoneHook<T>(wrapper: InstancePerContext<T>): () => void {
|
||||
let done: () => void;
|
||||
wrapper.donePromise = new Promise<void>((resolve, reject) => {
|
||||
done = resolve;
|
||||
});
|
||||
wrapper.isPending = true;
|
||||
return done;
|
||||
}
|
||||
|
||||
public async loadInstance<T>(
|
||||
wrapper: InstanceWrapper<T>,
|
||||
collection: Map<string, InstanceWrapper>,
|
||||
@@ -225,11 +140,91 @@ export class Injector {
|
||||
);
|
||||
}
|
||||
|
||||
public async loadMiddleware(
|
||||
wrapper: InstanceWrapper,
|
||||
collection: Map<string, InstanceWrapper>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const { metatype } = wrapper;
|
||||
const targetWrapper = collection.get(metatype.name);
|
||||
if (!isUndefined(targetWrapper.instance)) {
|
||||
return;
|
||||
}
|
||||
targetWrapper.instance = Object.create(metatype.prototype);
|
||||
await this.loadInstance(
|
||||
wrapper,
|
||||
collection,
|
||||
moduleRef,
|
||||
contextId,
|
||||
inquirer || wrapper,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadController(
|
||||
wrapper: InstanceWrapper<Controller>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
) {
|
||||
const controllers = moduleRef.controllers;
|
||||
await this.loadInstance<Controller>(
|
||||
wrapper,
|
||||
controllers,
|
||||
moduleRef,
|
||||
contextId,
|
||||
wrapper,
|
||||
);
|
||||
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
|
||||
}
|
||||
|
||||
public async loadInjectable<T = any>(
|
||||
wrapper: InstanceWrapper<T>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const injectables = moduleRef.injectables;
|
||||
await this.loadInstance<T>(
|
||||
wrapper,
|
||||
injectables,
|
||||
moduleRef,
|
||||
contextId,
|
||||
inquirer,
|
||||
);
|
||||
}
|
||||
|
||||
public async loadProvider(
|
||||
wrapper: InstanceWrapper<Injectable>,
|
||||
moduleRef: Module,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
) {
|
||||
const providers = moduleRef.providers;
|
||||
await this.loadInstance<Injectable>(
|
||||
wrapper,
|
||||
providers,
|
||||
moduleRef,
|
||||
contextId,
|
||||
inquirer,
|
||||
);
|
||||
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
|
||||
}
|
||||
|
||||
public applyDoneHook<T>(wrapper: InstancePerContext<T>): () => void {
|
||||
let done: () => void;
|
||||
wrapper.donePromise = new Promise<void>((resolve, reject) => {
|
||||
done = resolve;
|
||||
});
|
||||
wrapper.isPending = true;
|
||||
return done;
|
||||
}
|
||||
|
||||
public async resolveConstructorParams<T>(
|
||||
wrapper: InstanceWrapper<T>,
|
||||
moduleRef: Module,
|
||||
inject: InjectorDependency[],
|
||||
callback: (args: unknown[]) => void,
|
||||
callback: (args: unknown[]) => void | Promise<void>,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
parentInquirer?: InstanceWrapper,
|
||||
@@ -274,7 +269,7 @@ export class Injector {
|
||||
if (!instanceHost.isResolved && !paramWrapper.forwardRef) {
|
||||
isResolved = false;
|
||||
}
|
||||
return instanceHost && instanceHost.instance;
|
||||
return instanceHost?.instance;
|
||||
} catch (err) {
|
||||
const isOptional = optionalDependenciesIds.includes(index);
|
||||
if (!isOptional) {
|
||||
@@ -372,7 +367,7 @@ export class Injector {
|
||||
|
||||
public async resolveComponentHost<T>(
|
||||
moduleRef: Module,
|
||||
instanceWrapper: InstanceWrapper<T>,
|
||||
instanceWrapper: InstanceWrapper<T | Promise<T>>,
|
||||
contextId = STATIC_CONTEXT,
|
||||
inquirer?: InstanceWrapper,
|
||||
): Promise<InstanceWrapper> {
|
||||
|
||||
@@ -47,11 +47,15 @@ export class ModuleTokenFactory {
|
||||
|
||||
private replacer(key: string, value: any) {
|
||||
if (typeof value === 'function') {
|
||||
const isClass = /^class\s/.test(Function.prototype.toString.call(value));
|
||||
const funcAsString = value.toString();
|
||||
const isClass = /^class\s/.test(funcAsString);
|
||||
if (isClass) {
|
||||
return value.name;
|
||||
}
|
||||
return hash(value.toString(), { ignoreUnknown: true });
|
||||
return hash(funcAsString, { ignoreUnknown: true });
|
||||
}
|
||||
if (typeof value === 'symbol') {
|
||||
return value.toString();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
} from '@nestjs/common/interfaces/middleware/middleware-configuration.interface';
|
||||
import { NestMiddleware } from '@nestjs/common/interfaces/middleware/nest-middleware.interface';
|
||||
import { NestModule } from '@nestjs/common/interfaces/modules/nest-module.interface';
|
||||
import { isUndefined, validatePath } from '@nestjs/common/utils/shared.utils';
|
||||
import {
|
||||
addLeadingSlash,
|
||||
isUndefined,
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import { ApplicationConfig } from '../application-config';
|
||||
import { InvalidMiddlewareException } from '../errors/exceptions/invalid-middleware.exception';
|
||||
import { RuntimeException } from '../errors/exceptions/runtime.exception';
|
||||
@@ -165,6 +168,9 @@ export class MiddlewareModule {
|
||||
if (isUndefined(instanceWrapper)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
if (instanceWrapper.isTransient) {
|
||||
return;
|
||||
}
|
||||
await this.bindHandler(
|
||||
instanceWrapper,
|
||||
applicationRef,
|
||||
@@ -265,7 +271,7 @@ export class MiddlewareModule {
|
||||
) => void,
|
||||
) {
|
||||
const prefix = this.config.getGlobalPrefix();
|
||||
const basePath = validatePath(prefix);
|
||||
const basePath = addLeadingSlash(prefix);
|
||||
if (basePath && path === '/*') {
|
||||
// strip slash when a wildcard is being used
|
||||
// and global prefix has been set
|
||||
|
||||
@@ -2,9 +2,9 @@ import { RequestMethod } from '@nestjs/common';
|
||||
import { PATH_METADATA } from '@nestjs/common/constants';
|
||||
import { RouteInfo, Type } from '@nestjs/common/interfaces';
|
||||
import {
|
||||
addLeadingSlash,
|
||||
isString,
|
||||
isUndefined,
|
||||
validatePath,
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import { NestContainer } from '../injector/container';
|
||||
import { MetadataScanner } from '../metadata-scanner';
|
||||
@@ -64,11 +64,11 @@ export class RoutesMapper {
|
||||
}
|
||||
|
||||
private validateGlobalPath(path: string): string {
|
||||
const prefix = validatePath(path);
|
||||
const prefix = addLeadingSlash(path);
|
||||
return prefix === '/' ? '' : prefix;
|
||||
}
|
||||
|
||||
private validateRoutePath(path: string): string {
|
||||
return validatePath(path);
|
||||
return addLeadingSlash(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
LogLevel,
|
||||
ShutdownSignal,
|
||||
} from '@nestjs/common';
|
||||
import { Abstract, Scope } from '@nestjs/common/interfaces';
|
||||
import { Abstract, DynamicModule, Scope } from '@nestjs/common/interfaces';
|
||||
import { Type } from '@nestjs/common/interfaces/type.interface';
|
||||
import { isEmpty } from '@nestjs/common/utils/shared.utils';
|
||||
import { iterate } from 'iterare';
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
callModuleInitHook,
|
||||
} from './hooks';
|
||||
import { ContextId } from './injector';
|
||||
import { ModuleCompiler } from './injector/compiler';
|
||||
import { NestContainer } from './injector/container';
|
||||
import { Injector } from './injector/injector';
|
||||
import { InstanceLinksHost } from './injector/instance-links-host';
|
||||
@@ -33,8 +34,10 @@ import { Module } from './injector/module';
|
||||
export class NestApplicationContext implements INestApplicationContext {
|
||||
protected isInitialized = false;
|
||||
protected readonly injector = new Injector();
|
||||
private shutdownCleanupRef?: (...args: unknown[]) => unknown;
|
||||
|
||||
private readonly activeShutdownSignals = new Array<string>();
|
||||
private readonly moduleCompiler = new ModuleCompiler();
|
||||
private shutdownCleanupRef?: (...args: unknown[]) => unknown;
|
||||
private _instanceLinksHost: InstanceLinksHost;
|
||||
|
||||
private get instanceLinksHost() {
|
||||
@@ -55,14 +58,20 @@ export class NestApplicationContext implements INestApplicationContext {
|
||||
this.contextModule = modules.next().value;
|
||||
}
|
||||
|
||||
public select<T>(moduleType: Type<T>): INestApplicationContext {
|
||||
const modules = this.container.getModules();
|
||||
const moduleMetatype = this.contextModule.metatype;
|
||||
const scope = this.scope.concat(moduleMetatype);
|
||||
const moduleTokenFactory = this.container.getModuleTokenFactory();
|
||||
public select<T>(
|
||||
moduleType: Type<T> | DynamicModule,
|
||||
): INestApplicationContext {
|
||||
const modulesContainer = this.container.getModules();
|
||||
const contextModuleCtor = this.contextModule.metatype;
|
||||
const scope = this.scope.concat(contextModuleCtor);
|
||||
|
||||
const token = moduleTokenFactory.create(moduleType);
|
||||
const selectedModule = modules.get(token);
|
||||
const moduleTokenFactory = this.container.getModuleTokenFactory();
|
||||
const { type, dynamicMetadata } = this.moduleCompiler.extractMetadata(
|
||||
moduleType,
|
||||
);
|
||||
const token = moduleTokenFactory.create(type, dynamicMetadata);
|
||||
|
||||
const selectedModule = modulesContainer.get(token);
|
||||
if (!selectedModule) {
|
||||
throw new UnknownModuleException();
|
||||
}
|
||||
|
||||
@@ -9,11 +9,14 @@ import {
|
||||
PipeTransform,
|
||||
WebSocketAdapter,
|
||||
} from '@nestjs/common';
|
||||
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { isObject, validatePath } from '@nestjs/common/utils/shared.utils';
|
||||
import { addLeadingSlash, isObject } from '@nestjs/common/utils/shared.utils';
|
||||
import { iterate } from 'iterare';
|
||||
import { platform } from 'os';
|
||||
import { AbstractHttpAdapter } from './adapters';
|
||||
@@ -102,11 +105,15 @@ export class NestApplication
|
||||
if (!this.appOptions || !this.appOptions.cors) {
|
||||
return undefined;
|
||||
}
|
||||
const isCorsOptionsObj = isObject(this.appOptions.cors);
|
||||
if (!isCorsOptionsObj) {
|
||||
const passCustomOptions =
|
||||
isObject(this.appOptions.cors) ||
|
||||
typeof this.appOptions.cors === 'function';
|
||||
if (!passCustomOptions) {
|
||||
return this.enableCors();
|
||||
}
|
||||
return this.enableCors(this.appOptions.cors as CorsOptions);
|
||||
return this.enableCors(
|
||||
this.appOptions.cors as CorsOptions | CorsOptionsDelegate<any>,
|
||||
);
|
||||
}
|
||||
|
||||
public createServer<T = any>(): T {
|
||||
@@ -164,7 +171,7 @@ export class NestApplication
|
||||
await this.registerMiddleware(this.httpAdapter);
|
||||
|
||||
const prefix = this.config.getGlobalPrefix();
|
||||
const basePath = validatePath(prefix);
|
||||
const basePath = addLeadingSlash(prefix);
|
||||
this.routesResolver.resolve(this.httpAdapter, basePath);
|
||||
}
|
||||
|
||||
@@ -224,7 +231,7 @@ export class NestApplication
|
||||
return this;
|
||||
}
|
||||
|
||||
public enableCors(options?: CorsOptions): void {
|
||||
public enableCors(options?: CorsOptions | CorsOptionsDelegate<any>): void {
|
||||
this.httpAdapter.enableCors(options);
|
||||
}
|
||||
|
||||
@@ -239,8 +246,8 @@ export class NestApplication
|
||||
): Promise<any>;
|
||||
public async listen(port: number | string, ...args: any[]): Promise<any> {
|
||||
!this.isInitialized && (await this.init());
|
||||
this.httpAdapter.listen(port, ...args);
|
||||
this.isListening = true;
|
||||
this.httpAdapter.listen(port, ...args);
|
||||
return this.httpServer;
|
||||
}
|
||||
|
||||
@@ -348,7 +355,7 @@ export class NestApplication
|
||||
}
|
||||
|
||||
private listenToPromise(microservice: INestMicroservice) {
|
||||
return new Promise(async resolve => {
|
||||
return new Promise<void>(async resolve => {
|
||||
await microservice.listen(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,10 +215,10 @@ export class NestFactoryStatic {
|
||||
}
|
||||
|
||||
private applyLogger(options: NestApplicationContextOptions | undefined) {
|
||||
if (!options) {
|
||||
if (!options || options?.logger === true || isNil(options?.logger)) {
|
||||
return;
|
||||
}
|
||||
!isNil(options.logger) && Logger.overrideLogger(options.logger);
|
||||
Logger.overrideLogger(options.logger);
|
||||
}
|
||||
|
||||
private createHttpAdapter<T = any>(httpServer?: T): AbstractHttpAdapter {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/core",
|
||||
"version": "7.5.5",
|
||||
"version": "7.6.7",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -30,17 +30,31 @@
|
||||
"@nuxtjs/opencollective": "0.3.2",
|
||||
"fast-safe-stringify": "2.0.7",
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "2.0.3",
|
||||
"object-hash": "2.1.1",
|
||||
"path-to-regexp": "3.2.0",
|
||||
"tslib": "2.0.3",
|
||||
"uuid": "8.3.1"
|
||||
"tslib": "2.1.0",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "7.5.5"
|
||||
"@nestjs/common": "7.6.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.0.0",
|
||||
"@nestjs/microservices": "^7.0.0",
|
||||
"@nestjs/platform-express": "^7.0.0",
|
||||
"@nestjs/websockets": "^7.0.0",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/websockets": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/microservices": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/platform-express": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Controller } from '@nestjs/common/interfaces/controllers/controller.int
|
||||
import { Type } from '@nestjs/common/interfaces/type.interface';
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import {
|
||||
addLeadingSlash,
|
||||
isString,
|
||||
isUndefined,
|
||||
validatePath,
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import * as pathToRegexp from 'path-to-regexp';
|
||||
import { ApplicationConfig } from '../application-config';
|
||||
@@ -87,20 +87,20 @@ export class RouterExplorer {
|
||||
);
|
||||
}
|
||||
|
||||
public extractRouterPath(
|
||||
metatype: Type<Controller>,
|
||||
prefix?: string,
|
||||
): string {
|
||||
public extractRouterPath(metatype: Type<Controller>, prefix = ''): string[] {
|
||||
let path = Reflect.getMetadata(PATH_METADATA, metatype);
|
||||
if (prefix) path = prefix + this.validateRoutePath(path);
|
||||
return this.validateRoutePath(path);
|
||||
}
|
||||
|
||||
public validateRoutePath(path: string): string {
|
||||
if (isUndefined(path)) {
|
||||
throw new UnknownRequestMappingException();
|
||||
}
|
||||
return validatePath(path);
|
||||
|
||||
if (Array.isArray(path)) {
|
||||
path = path.map(p => prefix + addLeadingSlash(p));
|
||||
} else {
|
||||
path = [prefix + addLeadingSlash(path)];
|
||||
}
|
||||
|
||||
return path.map(p => addLeadingSlash(p));
|
||||
}
|
||||
|
||||
public scanForPaths(
|
||||
@@ -134,8 +134,8 @@ export class RouterExplorer {
|
||||
targetCallback,
|
||||
);
|
||||
const path = isString(routePath)
|
||||
? [this.validateRoutePath(routePath)]
|
||||
: routePath.map(p => this.validateRoutePath(p));
|
||||
? [addLeadingSlash(routePath)]
|
||||
: routePath.map(p => addLeadingSlash(p));
|
||||
return {
|
||||
path,
|
||||
requestMethod,
|
||||
|
||||
@@ -60,24 +60,27 @@ export class RoutesResolver implements Resolver {
|
||||
const { metatype } = instanceWrapper;
|
||||
|
||||
const host = this.getHostMetadata(metatype);
|
||||
const path = this.routerExplorer.extractRouterPath(
|
||||
const paths = this.routerExplorer.extractRouterPath(
|
||||
metatype as Type<any>,
|
||||
basePath,
|
||||
);
|
||||
const controllerName = metatype.name;
|
||||
this.logger.log(
|
||||
CONTROLLER_MAPPING_MESSAGE(
|
||||
controllerName,
|
||||
this.routerExplorer.stripEndSlash(path),
|
||||
),
|
||||
);
|
||||
this.routerExplorer.explore(
|
||||
instanceWrapper,
|
||||
moduleName,
|
||||
applicationRef,
|
||||
path,
|
||||
host,
|
||||
);
|
||||
|
||||
paths.forEach(path => {
|
||||
this.logger.log(
|
||||
CONTROLLER_MAPPING_MESSAGE(
|
||||
controllerName,
|
||||
this.routerExplorer.stripEndSlash(path),
|
||||
),
|
||||
);
|
||||
this.routerExplorer.explore(
|
||||
instanceWrapper,
|
||||
moduleName,
|
||||
applicationRef,
|
||||
path,
|
||||
host,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export class SseStream extends Transform {
|
||||
Connection: 'keep-alive',
|
||||
// Disable cache, even for old browsers and proxies
|
||||
'Cache-Control':
|
||||
'private, no-cache, no-store, must-revalidate, max-age=0',
|
||||
'private, no-cache, no-store, must-revalidate, max-age=0, no-transform',
|
||||
'Transfer-Encoding': 'identity',
|
||||
Pragma: 'no-cache',
|
||||
Expire: '0',
|
||||
|
||||
@@ -37,14 +37,14 @@ import { iterate } from 'iterare';
|
||||
import { ApplicationConfig } from './application-config';
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from './constants';
|
||||
import { CircularDependencyException } from './errors/exceptions/circular-dependency.exception';
|
||||
import { InvalidModuleException } from './errors/exceptions/invalid-module.exception';
|
||||
import { UndefinedModuleException } from './errors/exceptions/undefined-module.exception';
|
||||
import { getClassScope } from './helpers/get-class-scope';
|
||||
import { ModulesContainer } from './injector';
|
||||
import { NestContainer } from './injector/container';
|
||||
import { InstanceWrapper } from './injector/instance-wrapper';
|
||||
import { Module } from './injector/module';
|
||||
import { MetadataScanner } from './metadata-scanner';
|
||||
import { InvalidModuleException } from './errors/exceptions/invalid-module.exception';
|
||||
import { UndefinedModuleException } from './errors/exceptions/undefined-module.exception';
|
||||
|
||||
interface ApplicationProviderWrapper {
|
||||
moduleKey: string;
|
||||
@@ -72,40 +72,53 @@ export class DependenciesScanner {
|
||||
}
|
||||
|
||||
public async scanForModules(
|
||||
module: ForwardReference | Type<unknown> | DynamicModule,
|
||||
moduleDefinition:
|
||||
| ForwardReference
|
||||
| Type<unknown>
|
||||
| DynamicModule
|
||||
| Promise<DynamicModule>,
|
||||
scope: Type<unknown>[] = [],
|
||||
ctxRegistry: (ForwardReference | DynamicModule | Type<unknown>)[] = [],
|
||||
): Promise<Module> {
|
||||
const moduleInstance = await this.insertModule(module, scope);
|
||||
ctxRegistry.push(module);
|
||||
const moduleInstance = await this.insertModule(moduleDefinition, scope);
|
||||
moduleDefinition =
|
||||
moduleDefinition instanceof Promise
|
||||
? await moduleDefinition
|
||||
: moduleDefinition;
|
||||
ctxRegistry.push(moduleDefinition);
|
||||
|
||||
if (this.isForwardReference(module)) {
|
||||
module = (module as ForwardReference).forwardRef();
|
||||
if (this.isForwardReference(moduleDefinition)) {
|
||||
moduleDefinition = (moduleDefinition as ForwardReference).forwardRef();
|
||||
}
|
||||
const modules = !this.isDynamicModule(module as Type<any> | DynamicModule)
|
||||
? this.reflectMetadata(module as Type<any>, MODULE_METADATA.IMPORTS)
|
||||
const modules = !this.isDynamicModule(
|
||||
moduleDefinition as Type<any> | DynamicModule,
|
||||
)
|
||||
? this.reflectMetadata(
|
||||
moduleDefinition as Type<any>,
|
||||
MODULE_METADATA.IMPORTS,
|
||||
)
|
||||
: [
|
||||
...this.reflectMetadata(
|
||||
(module as DynamicModule).module,
|
||||
(moduleDefinition as DynamicModule).module,
|
||||
MODULE_METADATA.IMPORTS,
|
||||
),
|
||||
...((module as DynamicModule).imports || []),
|
||||
...((moduleDefinition as DynamicModule).imports || []),
|
||||
];
|
||||
|
||||
for (const [index, innerModule] of modules.entries()) {
|
||||
// In case of a circular dependency (ES module system), JavaScript will resolve the type to `undefined`.
|
||||
if (innerModule === undefined) {
|
||||
throw new UndefinedModuleException(module, index, scope);
|
||||
throw new UndefinedModuleException(moduleDefinition, index, scope);
|
||||
}
|
||||
if (!innerModule) {
|
||||
throw new InvalidModuleException(module, index, scope);
|
||||
throw new InvalidModuleException(moduleDefinition, index, scope);
|
||||
}
|
||||
if (ctxRegistry.includes(innerModule)) {
|
||||
continue;
|
||||
}
|
||||
await this.scanForModules(
|
||||
innerModule,
|
||||
[].concat(scope, module),
|
||||
[].concat(scope, moduleDefinition),
|
||||
ctxRegistry,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,28 +189,28 @@ describe('Injector', () => {
|
||||
});
|
||||
|
||||
describe('loadMiddleware', () => {
|
||||
let resolveConstructorParams: sinon.SinonSpy;
|
||||
let loadInstanceSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
resolveConstructorParams = sinon.spy();
|
||||
injector.resolveConstructorParams = resolveConstructorParams;
|
||||
loadInstanceSpy = sinon.spy();
|
||||
injector.loadInstance = loadInstanceSpy;
|
||||
});
|
||||
|
||||
it('should call "resolveConstructorParams" when instance is not resolved', () => {
|
||||
it('should call "loadInstance" when instance is not resolved', () => {
|
||||
const collection = {
|
||||
get: (...args) => ({}),
|
||||
set: (...args) => {},
|
||||
};
|
||||
|
||||
injector.loadMiddleware(
|
||||
{ metatype: { name: '' } } as any,
|
||||
{ metatype: { name: '', prototype: {} } } as any,
|
||||
collection as any,
|
||||
null,
|
||||
);
|
||||
expect(resolveConstructorParams.called).to.be.true;
|
||||
expect(loadInstanceSpy.called).to.be.true;
|
||||
});
|
||||
|
||||
it('should not call "resolveConstructorParams" when instance is not resolved', () => {
|
||||
it('should not call "loadInstanceSpy" when instance is not resolved', () => {
|
||||
const collection = {
|
||||
get: (...args) => ({
|
||||
instance: {},
|
||||
@@ -223,7 +223,7 @@ describe('Injector', () => {
|
||||
collection as any,
|
||||
null,
|
||||
);
|
||||
expect(resolveConstructorParams.called).to.be.false;
|
||||
expect(loadInstanceSpy.called).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('ModuleTokenFactory', () => {
|
||||
const token = factory.create(type, {
|
||||
providers: [{}],
|
||||
} as any);
|
||||
|
||||
expect(token).to.be.deep.eq(
|
||||
hash({
|
||||
id: moduleId,
|
||||
@@ -62,6 +63,24 @@ describe('ModuleTokenFactory', () => {
|
||||
'{"providers":["Provider"],"exports":["Provider"]}',
|
||||
);
|
||||
});
|
||||
it('should serialize symbols in a dynamic metadata object', () => {
|
||||
const metadata = {
|
||||
providers: [
|
||||
{
|
||||
provide: Symbol('a'),
|
||||
useValue: 'a',
|
||||
},
|
||||
{
|
||||
provide: Symbol('b'),
|
||||
useValue: 'b',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(factory.getDynamicMetadataToken(metadata)).to.be.eql(
|
||||
'{"providers":[{"provide":"Symbol(a)","useValue":"a"},{"provide":"Symbol(b)","useValue":"b"}]}',
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('when metadata does not exist', () => {
|
||||
it('should return empty string', () => {
|
||||
|
||||
@@ -32,6 +32,21 @@ describe('RouterExplorer', () => {
|
||||
public getTestUsingArray() {}
|
||||
}
|
||||
|
||||
@Controller(['global', 'global-alias'])
|
||||
class TestRouteAlias {
|
||||
@Get('test')
|
||||
public getTest() {}
|
||||
|
||||
@Post('test')
|
||||
public postTest() {}
|
||||
|
||||
@All('another-test')
|
||||
public anotherTest() {}
|
||||
|
||||
@Get(['foo', 'bar'])
|
||||
public getTestUsingArray() {}
|
||||
}
|
||||
|
||||
let routerBuilder: RouterExplorer;
|
||||
let injector: Injector;
|
||||
let exceptionsFilter: RouterExceptionFilters;
|
||||
@@ -70,6 +85,22 @@ describe('RouterExplorer', () => {
|
||||
expect(paths[2].requestMethod).to.eql(RequestMethod.ALL);
|
||||
expect(paths[3].requestMethod).to.eql(RequestMethod.GET);
|
||||
});
|
||||
|
||||
it('should method return expected list of route paths alias', () => {
|
||||
const paths = routerBuilder.scanForPaths(new TestRouteAlias());
|
||||
|
||||
expect(paths).to.have.length(4);
|
||||
|
||||
expect(paths[0].path).to.eql(['/test']);
|
||||
expect(paths[1].path).to.eql(['/test']);
|
||||
expect(paths[2].path).to.eql(['/another-test']);
|
||||
expect(paths[3].path).to.eql(['/foo', '/bar']);
|
||||
|
||||
expect(paths[0].requestMethod).to.eql(RequestMethod.GET);
|
||||
expect(paths[1].requestMethod).to.eql(RequestMethod.POST);
|
||||
expect(paths[2].requestMethod).to.eql(RequestMethod.ALL);
|
||||
expect(paths[3].requestMethod).to.eql(RequestMethod.GET);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exploreMethodMetadata', () => {
|
||||
@@ -87,6 +118,20 @@ describe('RouterExplorer', () => {
|
||||
expect(route.requestMethod).to.eql(RequestMethod.GET);
|
||||
});
|
||||
|
||||
it('should method return expected object which represent single route with alias', () => {
|
||||
const instance = new TestRouteAlias();
|
||||
const instanceProto = Object.getPrototypeOf(instance);
|
||||
|
||||
const route = routerBuilder.exploreMethodMetadata(
|
||||
new TestRouteAlias(),
|
||||
instanceProto,
|
||||
'getTest',
|
||||
);
|
||||
|
||||
expect(route.path).to.eql(['/test']);
|
||||
expect(route.requestMethod).to.eql(RequestMethod.GET);
|
||||
});
|
||||
|
||||
it('should method return expected object which represent multiple routes', () => {
|
||||
const instance = new TestRoute();
|
||||
const instanceProto = Object.getPrototypeOf(instance);
|
||||
@@ -100,6 +145,20 @@ describe('RouterExplorer', () => {
|
||||
expect(route.path).to.eql(['/foo', '/bar']);
|
||||
expect(route.requestMethod).to.eql(RequestMethod.GET);
|
||||
});
|
||||
|
||||
it('should method return expected object which represent multiple routes with alias', () => {
|
||||
const instance = new TestRouteAlias();
|
||||
const instanceProto = Object.getPrototypeOf(instance);
|
||||
|
||||
const route = routerBuilder.exploreMethodMetadata(
|
||||
new TestRouteAlias(),
|
||||
instanceProto,
|
||||
'getTestUsingArray',
|
||||
);
|
||||
|
||||
expect(route.path).to.eql(['/foo', '/bar']);
|
||||
expect(route.requestMethod).to.eql(RequestMethod.GET);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyPathsToRouterProxy', () => {
|
||||
@@ -130,14 +189,20 @@ describe('RouterExplorer', () => {
|
||||
|
||||
describe('extractRouterPath', () => {
|
||||
it('should return expected path', () => {
|
||||
expect(routerBuilder.extractRouterPath(TestRoute)).to.be.eql('/global');
|
||||
expect(routerBuilder.extractRouterPath(TestRoute, '/module')).to.be.eql(
|
||||
expect(routerBuilder.extractRouterPath(TestRoute)).to.be.eql(['/global']);
|
||||
expect(routerBuilder.extractRouterPath(TestRoute, '/module')).to.be.eql([
|
||||
'/module/global',
|
||||
);
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw it a there is a bad path expected path', () => {
|
||||
expect(() => routerBuilder.validateRoutePath(undefined)).to.throw();
|
||||
it('should return expected path with alias', () => {
|
||||
expect(routerBuilder.extractRouterPath(TestRouteAlias)).to.be.eql([
|
||||
'/global',
|
||||
'/global-alias',
|
||||
]);
|
||||
expect(
|
||||
routerBuilder.extractRouterPath(TestRouteAlias, '/module'),
|
||||
).to.be.eql(['/module/global', '/module/global-alias']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('RoutesResolver', () => {
|
||||
|
||||
sinon
|
||||
.stub((routesResolver as any).routerExplorer, 'extractRouterPath')
|
||||
.callsFake(() => '');
|
||||
.callsFake(() => ['']);
|
||||
routesResolver.registerRouters(routes, moduleName, '', appInstance);
|
||||
|
||||
expect(exploreSpy.called).to.be.true;
|
||||
@@ -114,7 +114,7 @@ describe('RoutesResolver', () => {
|
||||
|
||||
sinon
|
||||
.stub((routesResolver as any).routerExplorer, 'extractRouterPath')
|
||||
.callsFake(() => '');
|
||||
.callsFake(() => ['']);
|
||||
routesResolver.registerRouters(routes, moduleName, '', appInstance);
|
||||
|
||||
expect(exploreSpy.called).to.be.true;
|
||||
|
||||
@@ -125,7 +125,7 @@ data: hello
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
'Cache-Control':
|
||||
'private, no-cache, no-store, must-revalidate, max-age=0',
|
||||
'private, no-cache, no-store, must-revalidate, max-age=0, no-transform',
|
||||
'Transfer-Encoding': 'identity',
|
||||
Pragma: 'no-cache',
|
||||
Expire: '0',
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from '../constants';
|
||||
import { KafkaResponseDeserializer } from '../deserializers/kafka-response.deserializer';
|
||||
import { KafkaHeaders } from '../enums';
|
||||
import { InvalidKafkaClientTopicPartitionException } from '../errors/invalid-kafka-client-topic-partition.exception';
|
||||
import { InvalidKafkaClientTopicException } from '../errors/invalid-kafka-client-topic.exception';
|
||||
import {
|
||||
BrokersFunction,
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
import {
|
||||
KafkaLogger,
|
||||
KafkaParser,
|
||||
KafkaRoundRobinPartitionAssigner,
|
||||
KafkaReplyPartitionAssigner,
|
||||
} from '../helpers';
|
||||
import {
|
||||
KafkaOptions,
|
||||
@@ -46,7 +45,7 @@ export class ClientKafka extends ClientProxy {
|
||||
protected producer: Producer = null;
|
||||
protected logger = new Logger(ClientKafka.name);
|
||||
protected responsePatterns: string[] = [];
|
||||
protected consumerAssignments: { [key: string]: number[] } = {};
|
||||
protected consumerAssignments: { [key: string]: number } = {};
|
||||
|
||||
protected brokers: string[] | BrokersFunction;
|
||||
protected clientId: string;
|
||||
@@ -59,17 +58,16 @@ export class ClientKafka extends ClientProxy {
|
||||
this.getOptionsProp(this.options, 'client') || ({} as KafkaConfig);
|
||||
const consumerOptions =
|
||||
this.getOptionsProp(this.options, 'consumer') || ({} as ConsumerConfig);
|
||||
const postfixId =
|
||||
this.getOptionsProp(this.options, 'postfixId') || '-client';
|
||||
|
||||
this.brokers = clientOptions.brokers || [KAFKA_DEFAULT_BROKER];
|
||||
|
||||
// Append a unique id to the clientId and groupId
|
||||
// so they don't collide with a microservices client
|
||||
this.clientId =
|
||||
(clientOptions.clientId || KAFKA_DEFAULT_CLIENT) +
|
||||
(clientOptions.clientIdPostfix || '-client');
|
||||
this.groupId =
|
||||
(consumerOptions.groupId || KAFKA_DEFAULT_GROUP) +
|
||||
(clientOptions.clientIdPostfix || '-client');
|
||||
(clientOptions.clientId || KAFKA_DEFAULT_CLIENT) + postfixId;
|
||||
this.groupId = (consumerOptions.groupId || KAFKA_DEFAULT_GROUP) + postfixId;
|
||||
|
||||
kafkaPackage = loadPackage('kafkajs', ClientKafka.name, () =>
|
||||
require('kafkajs'),
|
||||
@@ -99,11 +97,8 @@ export class ClientKafka extends ClientProxy {
|
||||
this.client = this.createClient();
|
||||
|
||||
const partitionAssigners = [
|
||||
(
|
||||
config: ConstructorParameters<
|
||||
typeof KafkaRoundRobinPartitionAssigner
|
||||
>[0],
|
||||
) => new KafkaRoundRobinPartitionAssigner(config),
|
||||
(config: ConstructorParameters<typeof KafkaReplyPartitionAssigner>[1]) =>
|
||||
new KafkaReplyPartitionAssigner(this, config),
|
||||
] as any[];
|
||||
|
||||
const consumerOptions = Object.assign(
|
||||
@@ -188,6 +183,10 @@ export class ClientKafka extends ClientProxy {
|
||||
};
|
||||
}
|
||||
|
||||
public getConsumerAssignments() {
|
||||
return this.consumerAssignments;
|
||||
}
|
||||
|
||||
protected dispatchEvent(packet: OutgoingEvent): Promise<any> {
|
||||
const pattern = this.normalizePattern(packet.pattern);
|
||||
const outgoingEvent = this.serializer.serialize(packet.data);
|
||||
@@ -202,17 +201,13 @@ export class ClientKafka extends ClientProxy {
|
||||
}
|
||||
|
||||
protected getReplyTopicPartition(topic: string): string {
|
||||
const topicAssignments = this.consumerAssignments[topic];
|
||||
if (isUndefined(topicAssignments)) {
|
||||
const minimumPartition = this.consumerAssignments[topic];
|
||||
if (isUndefined(minimumPartition)) {
|
||||
throw new InvalidKafkaClientTopicException(topic);
|
||||
}
|
||||
|
||||
// if the current member isn't listening to
|
||||
// any partitions on the topic then throw an error.
|
||||
if (isUndefined(topicAssignments[0])) {
|
||||
throw new InvalidKafkaClientTopicPartitionException(topic);
|
||||
}
|
||||
return topicAssignments[0].toString();
|
||||
// get the minimum partition
|
||||
return minimumPartition.toString();
|
||||
}
|
||||
|
||||
protected publish(
|
||||
@@ -241,7 +236,7 @@ export class ClientKafka extends ClientProxy {
|
||||
},
|
||||
this.options.send || {},
|
||||
);
|
||||
this.producer.send(message);
|
||||
this.producer.send(message).catch(err => callback({ err }));
|
||||
|
||||
return () => this.routingMap.delete(packet.id);
|
||||
} catch (err) {
|
||||
@@ -254,7 +249,18 @@ export class ClientKafka extends ClientProxy {
|
||||
}
|
||||
|
||||
protected setConsumerAssignments(data: ConsumerGroupJoinEvent): void {
|
||||
this.consumerAssignments = data.payload.memberAssignment;
|
||||
const consumerAssignments: { [key: string]: number } = {};
|
||||
|
||||
// only need to set the minimum
|
||||
Object.keys(data.payload.memberAssignment).forEach(memberId => {
|
||||
const minimumPartition = Math.min(
|
||||
...data.payload.memberAssignment[memberId],
|
||||
);
|
||||
|
||||
consumerAssignments[memberId] = minimumPartition;
|
||||
});
|
||||
|
||||
this.consumerAssignments = consumerAssignments;
|
||||
}
|
||||
|
||||
protected initializeSerializer(options: KafkaOptions['options']) {
|
||||
|
||||
@@ -157,7 +157,7 @@ export class ClientMqtt extends ClientProxy {
|
||||
const pattern = this.normalizePattern(packet.pattern);
|
||||
const serializedPacket = this.serializer.serialize(packet);
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
this.mqttClient.publish(pattern, JSON.stringify(serializedPacket), err =>
|
||||
err ? reject(err) : resolve(),
|
||||
),
|
||||
|
||||
@@ -111,7 +111,7 @@ export class ClientNats extends ClientProxy {
|
||||
const pattern = this.normalizePattern(packet.pattern);
|
||||
const serializedPacket = this.serializer.serialize(packet);
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
this.natsClient.publish(pattern, serializedPacket as any, err =>
|
||||
err ? reject(err) : resolve(),
|
||||
),
|
||||
|
||||
@@ -190,7 +190,7 @@ export class ClientRedis extends ClientProxy {
|
||||
const pattern = this.normalizePattern(packet.pattern);
|
||||
const serializedPacket = this.serializer.serialize(packet);
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
this.pubClient.publish(pattern, JSON.stringify(serializedPacket), err =>
|
||||
err ? reject(err) : resolve(),
|
||||
),
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
RQM_DEFAULT_QUEUE_OPTIONS,
|
||||
RQM_DEFAULT_URL,
|
||||
} from '../constants';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
import { ReadPacket, RmqOptions, WritePacket } from '../interfaces';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
|
||||
let rqmPackage: any = {};
|
||||
|
||||
@@ -204,14 +204,14 @@ export class ClientRMQ extends ClientProxy {
|
||||
protected dispatchEvent(packet: ReadPacket): Promise<any> {
|
||||
const serializedPacket = this.serializer.serialize(packet);
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
this.channel.sendToQueue(
|
||||
this.queue,
|
||||
Buffer.from(JSON.stringify(serializedPacket)),
|
||||
{
|
||||
persistent: this.persistent,
|
||||
},
|
||||
err => (err ? reject(err) : resolve()),
|
||||
(err: unknown) => (err ? reject(err) : resolve()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,9 +41,7 @@ export interface RpcHandlerMetadata {
|
||||
export class RpcContextCreator {
|
||||
private readonly contextUtils = new ContextUtils();
|
||||
private readonly rpcParamsFactory = new RpcParamsFactory();
|
||||
private readonly handlerMetadataStorage = new HandlerMetadataStorage<
|
||||
RpcHandlerMetadata
|
||||
>();
|
||||
private readonly handlerMetadataStorage = new HandlerMetadataStorage<RpcHandlerMetadata>();
|
||||
|
||||
constructor(
|
||||
private readonly rpcProxy: RpcProxy,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { RuntimeException } from '@nestjs/core/errors/exceptions/runtime.exception';
|
||||
|
||||
export class InvalidKafkaClientTopicPartitionException extends RuntimeException {
|
||||
constructor(topic?: string) {
|
||||
super(
|
||||
`The client consumer subscribed to the topic (${topic}) which is not assigned to any partitions.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,931 +0,0 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as net from 'net';
|
||||
import * as tls from 'tls';
|
||||
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
type XOR<T, U> = T | U extends object
|
||||
? (Without<T, U> & U) | (Without<U, T> & T)
|
||||
: T | U;
|
||||
|
||||
export declare class Kafka {
|
||||
constructor(config: KafkaConfig);
|
||||
producer(config?: ProducerConfig): Producer;
|
||||
consumer(config?: ConsumerConfig): Consumer;
|
||||
admin(config?: AdminConfig): Admin;
|
||||
logger(): Logger;
|
||||
}
|
||||
|
||||
export type BrokersFunction = () => string[] | Promise<string[]>;
|
||||
|
||||
export interface KafkaConfig {
|
||||
brokers: string[] | BrokersFunction;
|
||||
ssl?: tls.ConnectionOptions | boolean;
|
||||
sasl?: SASLOptions;
|
||||
clientId?: string;
|
||||
clientIdPostfix?: string;
|
||||
connectionTimeout?: number;
|
||||
authenticationTimeout?: number;
|
||||
reauthenticationThreshold?: number;
|
||||
requestTimeout?: number;
|
||||
enforceRequestTimeout?: boolean;
|
||||
retry?: RetryOptions;
|
||||
socketFactory?: ISocketFactory;
|
||||
logLevel?: logLevel;
|
||||
logCreator?: logCreator;
|
||||
}
|
||||
|
||||
export type ISocketFactory = (
|
||||
host: string,
|
||||
port: number,
|
||||
ssl: tls.ConnectionOptions,
|
||||
onConnect: () => void,
|
||||
) => net.Socket;
|
||||
|
||||
export type SASLMechanism = 'plain' | 'scram-sha-256' | 'scram-sha-512' | 'aws';
|
||||
|
||||
export interface SASLOptions {
|
||||
mechanism: SASLMechanism;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ProducerConfig {
|
||||
createPartitioner?: ICustomPartitioner;
|
||||
retry?: RetryOptions;
|
||||
metadataMaxAge?: number;
|
||||
allowAutoTopicCreation?: boolean;
|
||||
idempotent?: boolean;
|
||||
transactionalId?: string;
|
||||
transactionTimeout?: number;
|
||||
maxInFlightRequests?: number;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
key?: Buffer | string | null;
|
||||
value: Buffer | string | null;
|
||||
partition?: number;
|
||||
headers?: IHeaders;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface PartitionerArgs {
|
||||
topic: string;
|
||||
partitionMetadata: PartitionMetadata[];
|
||||
message: Message;
|
||||
}
|
||||
|
||||
export type ICustomPartitioner = () => (args: PartitionerArgs) => number;
|
||||
export type DefaultPartitioner = ICustomPartitioner;
|
||||
export type JavaCompatiblePartitioner = ICustomPartitioner;
|
||||
|
||||
export let Partitioners: {
|
||||
DefaultPartitioner: DefaultPartitioner;
|
||||
JavaCompatiblePartitioner: JavaCompatiblePartitioner;
|
||||
};
|
||||
|
||||
export type PartitionMetadata = {
|
||||
partitionErrorCode: number;
|
||||
partitionId: number;
|
||||
leader: number;
|
||||
replicas: number[];
|
||||
isr: number[];
|
||||
offlineReplicas?: number[];
|
||||
};
|
||||
|
||||
export interface IHeaders {
|
||||
[key: string]: Buffer | string;
|
||||
}
|
||||
|
||||
export interface ConsumerConfig {
|
||||
groupId: string;
|
||||
partitionAssigners?: PartitionAssigner[];
|
||||
metadataMaxAge?: number;
|
||||
sessionTimeout?: number;
|
||||
rebalanceTimeout?: number;
|
||||
heartbeatInterval?: number;
|
||||
maxBytesPerPartition?: number;
|
||||
minBytes?: number;
|
||||
maxBytes?: number;
|
||||
maxWaitTimeInMs?: number;
|
||||
retry?: RetryOptions & {
|
||||
restartOnFailure?: (err: Error) => Promise<boolean>;
|
||||
};
|
||||
allowAutoTopicCreation?: boolean;
|
||||
maxInFlightRequests?: number;
|
||||
readUncommitted?: boolean;
|
||||
rackId?: string;
|
||||
}
|
||||
|
||||
export type PartitionAssigner = (config: { cluster: Cluster }) => Assigner;
|
||||
|
||||
export interface CoordinatorMetadata {
|
||||
errorCode: number;
|
||||
coordinator: {
|
||||
nodeId: number;
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type Cluster = {
|
||||
isConnected(): boolean;
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
refreshMetadata(): Promise<void>;
|
||||
refreshMetadataIfNecessary(): Promise<void>;
|
||||
addTargetTopic(topic: string): Promise<void>;
|
||||
findBroker(node: { nodeId: string }): Promise<Broker>;
|
||||
findControllerBroker(): Promise<Broker>;
|
||||
findTopicPartitionMetadata(topic: string): PartitionMetadata[];
|
||||
findLeaderForPartitions(
|
||||
topic: string,
|
||||
partitions: number[],
|
||||
): { [leader: string]: number[] };
|
||||
findGroupCoordinator(group: { groupId: string }): Promise<Broker>;
|
||||
findGroupCoordinatorMetadata(group: {
|
||||
groupId: string;
|
||||
}): Promise<CoordinatorMetadata>;
|
||||
defaultOffset(config: { fromBeginning: boolean }): number;
|
||||
fetchTopicsOffset(
|
||||
topics: Array<
|
||||
{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number }>;
|
||||
} & XOR<{ fromBeginning: boolean }, { fromTimestamp: number }>
|
||||
>,
|
||||
): Promise<{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number; offset: string }>;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type Assignment = { [topic: string]: number[] };
|
||||
|
||||
export type GroupMember = { memberId: string; memberMetadata: Buffer };
|
||||
|
||||
export type GroupMemberAssignment = {
|
||||
memberId: string;
|
||||
memberAssignment: Buffer;
|
||||
};
|
||||
|
||||
export type GroupState = { name: string; metadata: Buffer };
|
||||
|
||||
export type Assigner = {
|
||||
name: string;
|
||||
version: number;
|
||||
assign(group: {
|
||||
members: GroupMember[];
|
||||
topics: string[];
|
||||
}): Promise<GroupMemberAssignment[]>;
|
||||
protocol(subscription: { topics: string[] }): GroupState;
|
||||
};
|
||||
|
||||
export interface RetryOptions {
|
||||
maxRetryTime?: number;
|
||||
initialRetryTime?: number;
|
||||
factor?: number;
|
||||
multiplier?: number;
|
||||
retries?: number;
|
||||
}
|
||||
|
||||
export interface AdminConfig {
|
||||
retry?: RetryOptions;
|
||||
}
|
||||
|
||||
export interface ITopicConfig {
|
||||
topic: string;
|
||||
numPartitions?: number;
|
||||
replicationFactor?: number;
|
||||
replicaAssignment?: object[];
|
||||
configEntries?: object[];
|
||||
}
|
||||
|
||||
export interface ITopicPartitionConfig {
|
||||
topic: string;
|
||||
count: number;
|
||||
assignments?: Array<Array<number>>;
|
||||
}
|
||||
|
||||
export interface ITopicMetadata {
|
||||
name: string;
|
||||
partitions: PartitionMetadata[];
|
||||
}
|
||||
|
||||
export enum ResourceTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
TOPIC = 2,
|
||||
GROUP = 3,
|
||||
CLUSTER = 4,
|
||||
TRANSACTIONAL_ID = 5,
|
||||
DELEGATION_TOKEN = 6,
|
||||
}
|
||||
|
||||
export interface ResourceConfigQuery {
|
||||
type: ResourceTypes;
|
||||
name: string;
|
||||
configNames?: string[];
|
||||
}
|
||||
|
||||
export interface ConfigEntries {
|
||||
configName: string;
|
||||
configValue: string;
|
||||
isDefault: boolean;
|
||||
isSensitive: boolean;
|
||||
readOnly: boolean;
|
||||
configSynonyms: ConfigSynonyms[];
|
||||
}
|
||||
|
||||
export interface ConfigSynonyms {
|
||||
configName: string;
|
||||
configValue: string;
|
||||
configSource: number;
|
||||
}
|
||||
|
||||
export interface DescribeConfigResponse {
|
||||
resources: {
|
||||
configEntries: ConfigEntries[];
|
||||
errorCode: number;
|
||||
errorMessage: string;
|
||||
resourceName: string;
|
||||
resourceType: ResourceTypes;
|
||||
}[];
|
||||
throttleTime: number;
|
||||
}
|
||||
|
||||
export interface IResourceConfig {
|
||||
type: ResourceTypes;
|
||||
name: string;
|
||||
configEntries: { name: string; value: string }[];
|
||||
}
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
||||
export type AdminEvents = {
|
||||
CONNECT: 'admin.connect';
|
||||
DISCONNECT: 'admin.disconnect';
|
||||
REQUEST: 'admin.network.request';
|
||||
REQUEST_TIMEOUT: 'admin.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'admin.network.request_queue_size';
|
||||
};
|
||||
|
||||
export interface InstrumentationEvent<T> {
|
||||
id: string;
|
||||
type: string;
|
||||
timestamp: number;
|
||||
payload: T;
|
||||
}
|
||||
|
||||
export type RemoveInstrumentationEventListener<T> = () => void;
|
||||
|
||||
export type ConnectEvent = InstrumentationEvent<null>;
|
||||
export type DisconnectEvent = InstrumentationEvent<null>;
|
||||
export type RequestEvent = InstrumentationEvent<{
|
||||
apiKey: number;
|
||||
apiName: string;
|
||||
apiVersion: number;
|
||||
broker: string;
|
||||
clientId: string;
|
||||
correlationId: number;
|
||||
createdAt: number;
|
||||
duration: number;
|
||||
pendingDuration: number;
|
||||
sentAt: number;
|
||||
size: number;
|
||||
}>;
|
||||
export type RequestTimeoutEvent = InstrumentationEvent<{
|
||||
apiKey: number;
|
||||
apiName: string;
|
||||
apiVersion: number;
|
||||
broker: string;
|
||||
clientId: string;
|
||||
correlationId: number;
|
||||
createdAt: number;
|
||||
pendingDuration: number;
|
||||
sentAt: number;
|
||||
}>;
|
||||
export type RequestQueueSizeEvent = InstrumentationEvent<{
|
||||
broker: string;
|
||||
clientId: string;
|
||||
queueSize: number;
|
||||
}>;
|
||||
|
||||
export interface SeekEntry {
|
||||
partition: number;
|
||||
offset: string;
|
||||
}
|
||||
|
||||
export type Admin = {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
listTopics(): Promise<string[]>;
|
||||
createTopics(options: {
|
||||
validateOnly?: boolean;
|
||||
waitForLeaders?: boolean;
|
||||
timeout?: number;
|
||||
topics: ITopicConfig[];
|
||||
}): Promise<boolean>;
|
||||
deleteTopics(options: { topics: string[]; timeout?: number }): Promise<void>;
|
||||
createPartitions(options: {
|
||||
validateOnly?: boolean;
|
||||
timeout?: number;
|
||||
topicPartitions: ITopicPartitionConfig[];
|
||||
}): Promise<boolean>;
|
||||
fetchTopicMetadata(options?: {
|
||||
topics: string[];
|
||||
}): Promise<{ topics: Array<ITopicMetadata> }>;
|
||||
fetchOffsets(options: {
|
||||
groupId: string;
|
||||
topic: string;
|
||||
}): Promise<Array<SeekEntry & { metadata: string | null }>>;
|
||||
fetchTopicOffsets(
|
||||
topic: string,
|
||||
): Promise<Array<SeekEntry & { high: string; low: string }>>;
|
||||
fetchTopicOffsetsByTimestamp(
|
||||
topic: string,
|
||||
timestamp?: number,
|
||||
): Promise<Array<SeekEntry>>;
|
||||
describeCluster(): Promise<{
|
||||
brokers: Array<{ nodeId: number; host: string; port: number }>;
|
||||
controller: number | null;
|
||||
clusterId: string;
|
||||
}>;
|
||||
setOffsets(options: {
|
||||
groupId: string;
|
||||
topic: string;
|
||||
partitions: SeekEntry[];
|
||||
}): Promise<void>;
|
||||
resetOffsets(options: {
|
||||
groupId: string;
|
||||
topic: string;
|
||||
earliest: boolean;
|
||||
}): Promise<void>;
|
||||
describeConfigs(configs: {
|
||||
resources: ResourceConfigQuery[];
|
||||
includeSynonyms: boolean;
|
||||
}): Promise<DescribeConfigResponse>;
|
||||
alterConfigs(configs: {
|
||||
validateOnly: boolean;
|
||||
resources: IResourceConfig[];
|
||||
}): Promise<any>;
|
||||
listGroups(): Promise<{ groups: GroupOverview[] }>;
|
||||
deleteGroups(groupIds: string[]): Promise<DeleteGroupsResult[]>;
|
||||
describeGroups(groupIds: string[]): Promise<GroupDescriptions>;
|
||||
logger(): Logger;
|
||||
on(
|
||||
eventName: ValueOf<AdminEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
events: AdminEvents;
|
||||
};
|
||||
|
||||
export let PartitionAssigners: { roundRobin: PartitionAssigner };
|
||||
|
||||
export interface ISerializer<T> {
|
||||
encode(value: T): Buffer;
|
||||
decode(buffer: Buffer): T | null;
|
||||
}
|
||||
|
||||
export type MemberMetadata = {
|
||||
version: number;
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
};
|
||||
|
||||
export type MemberAssignment = {
|
||||
version: number;
|
||||
assignment: Assignment;
|
||||
userData: Buffer;
|
||||
};
|
||||
|
||||
export let AssignerProtocol: {
|
||||
MemberMetadata: ISerializer<MemberMetadata>;
|
||||
MemberAssignment: ISerializer<MemberAssignment>;
|
||||
};
|
||||
|
||||
export enum logLevel {
|
||||
NOTHING = 0,
|
||||
ERROR = 1,
|
||||
WARN = 2,
|
||||
INFO = 4,
|
||||
DEBUG = 5,
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
namespace: string;
|
||||
level: logLevel;
|
||||
label: string;
|
||||
log: LoggerEntryContent;
|
||||
}
|
||||
|
||||
export interface LoggerEntryContent {
|
||||
readonly timestamp: Date;
|
||||
readonly message: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type logCreator = (logLevel: logLevel) => (entry: LogEntry) => void;
|
||||
|
||||
export type Logger = {
|
||||
info: (message: string, extra?: object) => void;
|
||||
error: (message: string, extra?: object) => void;
|
||||
warn: (message: string, extra?: object) => void;
|
||||
debug: (message: string, extra?: object) => void;
|
||||
};
|
||||
|
||||
export type Broker = {
|
||||
isConnected(): boolean;
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
apiVersions(): Promise<{
|
||||
[apiKey: number]: { minVersion: number; maxVersion: number };
|
||||
}>;
|
||||
metadata(
|
||||
topics: string[],
|
||||
): Promise<{
|
||||
brokers: Array<{
|
||||
nodeId: number;
|
||||
host: string;
|
||||
port: number;
|
||||
rack?: string;
|
||||
}>;
|
||||
topicMetadata: Array<{
|
||||
topicErrorCode: number;
|
||||
topic: number;
|
||||
partitionMetadata: PartitionMetadata[];
|
||||
}>;
|
||||
}>;
|
||||
offsetCommit(request: {
|
||||
groupId: string;
|
||||
groupGenerationId: number;
|
||||
memberId: string;
|
||||
retentionTime?: number;
|
||||
topics: Array<{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number; offset: string }>;
|
||||
}>;
|
||||
}): Promise<any>;
|
||||
fetch(request: {
|
||||
replicaId?: number;
|
||||
isolationLevel?: number;
|
||||
maxWaitTime?: number;
|
||||
minBytes?: number;
|
||||
maxBytes?: number;
|
||||
topics: Array<{
|
||||
topic: string;
|
||||
partitions: Array<{
|
||||
partition: number;
|
||||
fetchOffset: string;
|
||||
maxBytes: number;
|
||||
}>;
|
||||
}>;
|
||||
rackId?: string;
|
||||
}): Promise<any>;
|
||||
};
|
||||
|
||||
export type KafkaMessage = {
|
||||
key: Buffer;
|
||||
value: Buffer | null;
|
||||
timestamp: string;
|
||||
size: number;
|
||||
attributes: number;
|
||||
offset: string;
|
||||
headers?: IHeaders;
|
||||
};
|
||||
|
||||
export interface ProducerRecord {
|
||||
topic: string;
|
||||
messages: Message[];
|
||||
acks?: number;
|
||||
timeout?: number;
|
||||
compression?: CompressionTypes;
|
||||
}
|
||||
|
||||
export type RecordMetadata = {
|
||||
topicName: string;
|
||||
partition: number;
|
||||
errorCode: number;
|
||||
offset: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
export interface TopicMessages {
|
||||
topic: string;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface ProducerBatch {
|
||||
acks?: number;
|
||||
timeout?: number;
|
||||
compression?: CompressionTypes;
|
||||
topicMessages?: TopicMessages[];
|
||||
}
|
||||
|
||||
export interface PartitionOffset {
|
||||
partition: number;
|
||||
offset: string;
|
||||
}
|
||||
|
||||
export interface TopicOffsets {
|
||||
topic: string;
|
||||
partitions: PartitionOffset[];
|
||||
}
|
||||
|
||||
export interface Offsets {
|
||||
topics: TopicOffsets[];
|
||||
}
|
||||
|
||||
type Sender = {
|
||||
send(record: ProducerRecord): Promise<RecordMetadata[]>;
|
||||
sendBatch(batch: ProducerBatch): Promise<RecordMetadata[]>;
|
||||
};
|
||||
|
||||
export type ProducerEvents = {
|
||||
CONNECT: 'producer.connect';
|
||||
DISCONNECT: 'producer.disconnect';
|
||||
REQUEST: 'producer.network.request';
|
||||
REQUEST_TIMEOUT: 'producer.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'producer.network.request_queue_size';
|
||||
};
|
||||
|
||||
export type Producer = Sender & {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
isIdempotent(): boolean;
|
||||
events: ProducerEvents;
|
||||
on(
|
||||
eventName: ValueOf<ProducerEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
transaction(): Promise<Transaction>;
|
||||
logger(): Logger;
|
||||
};
|
||||
|
||||
export type Transaction = Sender & {
|
||||
sendOffsets(offsets: Offsets & { consumerGroupId: string }): Promise<void>;
|
||||
commit(): Promise<void>;
|
||||
abort(): Promise<void>;
|
||||
isActive(): boolean;
|
||||
};
|
||||
|
||||
export type ConsumerGroup = {
|
||||
groupId: string;
|
||||
generationId: number;
|
||||
memberId: string;
|
||||
coordinator: Broker;
|
||||
};
|
||||
|
||||
export type MemberDescription = {
|
||||
clientHost: string;
|
||||
clientId: string;
|
||||
memberId: string;
|
||||
memberAssignment: Buffer;
|
||||
memberMetadata: Buffer;
|
||||
};
|
||||
|
||||
export type GroupDescription = {
|
||||
groupId: string;
|
||||
members: MemberDescription[];
|
||||
protocol: string;
|
||||
protocolType: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
export type GroupDescriptions = {
|
||||
groups: GroupDescription[];
|
||||
};
|
||||
|
||||
export type TopicPartitions = { topic: string; partitions: number[] };
|
||||
export type TopicPartitionOffsetAndMetadata = {
|
||||
topic: string;
|
||||
partition: number;
|
||||
offset: string;
|
||||
metadata?: string | null;
|
||||
};
|
||||
|
||||
// TODO: Remove with 2.x
|
||||
export type TopicPartitionOffsetAndMedata = TopicPartitionOffsetAndMetadata;
|
||||
|
||||
export type Batch = {
|
||||
topic: string;
|
||||
partition: number;
|
||||
highWatermark: string;
|
||||
messages: KafkaMessage[];
|
||||
isEmpty(): boolean;
|
||||
firstOffset(): string | null;
|
||||
lastOffset(): string;
|
||||
offsetLag(): string;
|
||||
offsetLagLow(): string;
|
||||
};
|
||||
|
||||
export type GroupOverview = {
|
||||
groupId: string;
|
||||
protocolType: string;
|
||||
};
|
||||
|
||||
export type DeleteGroupsResult = {
|
||||
groupId: string;
|
||||
errorCode?: number;
|
||||
};
|
||||
|
||||
export type ConsumerEvents = {
|
||||
HEARTBEAT: 'consumer.heartbeat';
|
||||
COMMIT_OFFSETS: 'consumer.commit_offsets';
|
||||
GROUP_JOIN: 'consumer.group_join';
|
||||
FETCH_START: 'consumer.fetch_start';
|
||||
FETCH: 'consumer.fetch';
|
||||
START_BATCH_PROCESS: 'consumer.start_batch_process';
|
||||
END_BATCH_PROCESS: 'consumer.end_batch_process';
|
||||
CONNECT: 'consumer.connect';
|
||||
DISCONNECT: 'consumer.disconnect';
|
||||
STOP: 'consumer.stop';
|
||||
CRASH: 'consumer.crash';
|
||||
REQUEST: 'consumer.network.request';
|
||||
REQUEST_TIMEOUT: 'consumer.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'consumer.network.request_queue_size';
|
||||
};
|
||||
export type ConsumerHeartbeatEvent = InstrumentationEvent<{
|
||||
groupId: string;
|
||||
memberId: string;
|
||||
groupGenerationId: number;
|
||||
}>;
|
||||
export type ConsumerCommitOffsetsEvent = InstrumentationEvent<{
|
||||
groupId: string;
|
||||
memberId: string;
|
||||
groupGenerationId: number;
|
||||
topics: {
|
||||
topic: string;
|
||||
partitions: {
|
||||
offset: string;
|
||||
partition: string;
|
||||
}[];
|
||||
}[];
|
||||
}>;
|
||||
export interface IMemberAssignment {
|
||||
[key: string]: number[];
|
||||
}
|
||||
export type ConsumerGroupJoinEvent = InstrumentationEvent<{
|
||||
duration: number;
|
||||
groupId: string;
|
||||
isLeader: boolean;
|
||||
leaderId: string;
|
||||
groupProtocol: string;
|
||||
memberId: string;
|
||||
memberAssignment: IMemberAssignment;
|
||||
}>;
|
||||
export type ConsumerFetchEvent = InstrumentationEvent<{
|
||||
numberOfBatches: number;
|
||||
duration: number;
|
||||
}>;
|
||||
interface IBatchProcessEvent {
|
||||
topic: string;
|
||||
partition: number;
|
||||
highWatermark: string;
|
||||
offsetLag: string;
|
||||
offsetLagLow: string;
|
||||
batchSize: number;
|
||||
firstOffset: string;
|
||||
lastOffset: string;
|
||||
}
|
||||
export type ConsumerStartBatchProcessEvent = InstrumentationEvent<
|
||||
IBatchProcessEvent
|
||||
>;
|
||||
export type ConsumerEndBatchProcessEvent = InstrumentationEvent<
|
||||
IBatchProcessEvent & { duration: number }
|
||||
>;
|
||||
export type ConsumerCrashEvent = InstrumentationEvent<{
|
||||
error: Error;
|
||||
groupId: string;
|
||||
}>;
|
||||
|
||||
export interface OffsetsByTopicPartition {
|
||||
topics: TopicOffsets[];
|
||||
}
|
||||
|
||||
export interface EachMessagePayload {
|
||||
topic: string;
|
||||
partition: number;
|
||||
message: KafkaMessage;
|
||||
}
|
||||
|
||||
export interface EachBatchPayload {
|
||||
batch: Batch;
|
||||
resolveOffset(offset: string): void;
|
||||
heartbeat(): Promise<void>;
|
||||
commitOffsetsIfNecessary(offsets?: Offsets): Promise<void>;
|
||||
uncommittedOffsets(): OffsetsByTopicPartition;
|
||||
isRunning(): boolean;
|
||||
isStale(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type alias to keep compatibility with @types/kafkajs
|
||||
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/712ad9d59ccca6a3cc92f347fea0d1c7b02f5eeb/types/kafkajs/index.d.ts#L321-L325
|
||||
*/
|
||||
export type ConsumerEachMessagePayload = EachMessagePayload;
|
||||
|
||||
/**
|
||||
* Type alias to keep compatibility with @types/kafkajs
|
||||
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/712ad9d59ccca6a3cc92f347fea0d1c7b02f5eeb/types/kafkajs/index.d.ts#L327-L336
|
||||
*/
|
||||
export type ConsumerEachBatchPayload = EachBatchPayload;
|
||||
|
||||
export type ConsumerRunConfig = {
|
||||
autoCommit?: boolean;
|
||||
autoCommitInterval?: number | null;
|
||||
autoCommitThreshold?: number | null;
|
||||
eachBatchAutoResolve?: boolean;
|
||||
partitionsConsumedConcurrently?: number;
|
||||
eachBatch?: (payload: EachBatchPayload) => Promise<void>;
|
||||
eachMessage?: (payload: EachMessagePayload) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ConsumerSubscribeTopic = {
|
||||
topic: string | RegExp;
|
||||
fromBeginning?: boolean;
|
||||
};
|
||||
|
||||
export type Consumer = {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
subscribe(topic: ConsumerSubscribeTopic): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
run(config?: ConsumerRunConfig): Promise<void>;
|
||||
commitOffsets(
|
||||
topicPartitions: Array<TopicPartitionOffsetAndMetadata>,
|
||||
): Promise<void>;
|
||||
seek(topicPartition: {
|
||||
topic: string;
|
||||
partition: number;
|
||||
offset: string;
|
||||
}): void;
|
||||
describeGroup(): Promise<GroupDescription>;
|
||||
pause(topics: Array<{ topic: string; partitions?: number[] }>): void;
|
||||
paused(): TopicPartitions[];
|
||||
resume(topics: Array<{ topic: string; partitions?: number[] }>): void;
|
||||
on(
|
||||
eventName: ValueOf<ConsumerEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
logger(): Logger;
|
||||
events: ConsumerEvents;
|
||||
};
|
||||
|
||||
export enum CompressionTypes {
|
||||
None = 0,
|
||||
GZIP = 1,
|
||||
Snappy = 2,
|
||||
LZ4 = 3,
|
||||
ZSTD = 4,
|
||||
}
|
||||
|
||||
export let CompressionCodecs: {
|
||||
[CompressionTypes.GZIP]: () => any;
|
||||
[CompressionTypes.Snappy]: () => any;
|
||||
[CompressionTypes.LZ4]: () => any;
|
||||
[CompressionTypes.ZSTD]: () => any;
|
||||
};
|
||||
|
||||
export declare class KafkaJSError extends Error {
|
||||
constructor(e: Error | string, metadata?: KafkaJSErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSNonRetriableError extends KafkaJSError {
|
||||
constructor(e: Error | string);
|
||||
}
|
||||
|
||||
export declare class KafkaJSProtocolError extends KafkaJSError {
|
||||
constructor(e: Error | string);
|
||||
}
|
||||
|
||||
export declare class KafkaJSOffsetOutOfRange extends KafkaJSProtocolError {
|
||||
constructor(e: Error | string, metadata?: KafkaJSOffsetOutOfRangeMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSNumberOfRetriesExceeded extends KafkaJSNonRetriableError {
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSNumberOfRetriesExceededMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSConnectionError extends KafkaJSError {
|
||||
constructor(e: Error | string, metadata?: KafkaJSConnectionErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSRequestTimeoutError extends KafkaJSError {
|
||||
constructor(e: Error | string, metadata?: KafkaJSRequestTimeoutErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSMetadataNotLoaded extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSTopicMetadataNotLoaded extends KafkaJSMetadataNotLoaded {
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSTopicMetadataNotLoadedMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSStaleTopicMetadataAssignment extends KafkaJSError {
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSStaleTopicMetadataAssignmentMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSServerDoesNotSupportApiKey extends KafkaJSNonRetriableError {
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSServerDoesNotSupportApiKeyMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSBrokerNotFound extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSPartialMessageError extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSSASLAuthenticationError extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSGroupCoordinatorNotFound extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSNotImplemented extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSTimeout extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSLockTimeout extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSUnsupportedMagicByteInMessageSet extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSDeleteGroupsError extends KafkaJSError {
|
||||
constructor(e: Error | string, groups?: KafkaJSDeleteGroupsErrorGroups[]);
|
||||
}
|
||||
|
||||
export interface KafkaJSDeleteGroupsErrorGroups {
|
||||
groupId: string;
|
||||
errorCode: number;
|
||||
error: KafkaJSError;
|
||||
}
|
||||
|
||||
export interface KafkaJSErrorMetadata {
|
||||
retriable?: boolean;
|
||||
topic?: string;
|
||||
partitionId?: number;
|
||||
metadata?: PartitionMetadata;
|
||||
}
|
||||
|
||||
export interface KafkaJSOffsetOutOfRangeMetadata {
|
||||
topic: string;
|
||||
partition: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSNumberOfRetriesExceededMetadata {
|
||||
retryCount: number;
|
||||
retryTime: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSConnectionErrorMetadata {
|
||||
broker?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export interface KafkaJSRequestTimeoutErrorMetadata {
|
||||
broker: string;
|
||||
clientId: string;
|
||||
correlationId: number;
|
||||
createdAt: number;
|
||||
sentAt: number;
|
||||
pendingDuration: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSTopicMetadataNotLoadedMetadata {
|
||||
topic: string;
|
||||
}
|
||||
|
||||
export interface KafkaJSStaleTopicMetadataAssignmentMetadata {
|
||||
topic: string;
|
||||
unknownPartitions: PartitionMetadata[];
|
||||
}
|
||||
|
||||
export interface KafkaJSServerDoesNotSupportApiKeyMetadata {
|
||||
apiKey: number;
|
||||
apiName: string;
|
||||
}
|
||||
672
packages/microservices/external/kafka.interface.ts
vendored
672
packages/microservices/external/kafka.interface.ts
vendored
@@ -1,9 +1,19 @@
|
||||
/**
|
||||
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/kafkajs/index.d.ts
|
||||
* Do NOT add NestJS logic to this interface. It is meant to ONLY represent the types for the kafkajs package.
|
||||
*
|
||||
* @see https://github.com/tulios/kafkajs/blob/master/types/index.d.ts
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as net from 'net';
|
||||
import * as tls from 'tls';
|
||||
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
type XOR<T, U> = T | U extends object
|
||||
? (Without<T, U> & U) | (Without<U, T> & T)
|
||||
: T | U;
|
||||
|
||||
export declare class Kafka {
|
||||
constructor(config: KafkaConfig);
|
||||
producer(config?: ProducerConfig): Producer;
|
||||
@@ -19,7 +29,6 @@ export interface KafkaConfig {
|
||||
ssl?: tls.ConnectionOptions | boolean;
|
||||
sasl?: SASLOptions;
|
||||
clientId?: string;
|
||||
clientIdPostfix?: string;
|
||||
connectionTimeout?: number;
|
||||
authenticationTimeout?: number;
|
||||
reauthenticationThreshold?: number;
|
||||
@@ -31,19 +40,40 @@ export interface KafkaConfig {
|
||||
logCreator?: logCreator;
|
||||
}
|
||||
|
||||
export type ISocketFactory = (
|
||||
host: string,
|
||||
port: number,
|
||||
ssl: tls.ConnectionOptions,
|
||||
onConnect: () => void,
|
||||
) => net.Socket;
|
||||
|
||||
export interface SASLOptions {
|
||||
mechanism: 'plain' | 'scram-sha-256' | 'scram-sha-512' | 'aws';
|
||||
username: string;
|
||||
password: string;
|
||||
export interface ISocketFactoryArgs {
|
||||
host: string;
|
||||
port: number;
|
||||
ssl: tls.ConnectionOptions;
|
||||
onConnect: () => void;
|
||||
}
|
||||
|
||||
export type ISocketFactory = (args: ISocketFactoryArgs) => net.Socket;
|
||||
|
||||
export interface OauthbearerProviderResponse {
|
||||
value: string;
|
||||
}
|
||||
|
||||
type SASLMechanismOptionsMap = {
|
||||
plain: { username: string; password: string };
|
||||
'scram-sha-256': { username: string; password: string };
|
||||
'scram-sha-512': { username: string; password: string };
|
||||
aws: {
|
||||
authorizationIdentity: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
sessionToken?: string;
|
||||
};
|
||||
oauthbearer: {
|
||||
oauthBearerProvider: () => Promise<OauthbearerProviderResponse>;
|
||||
};
|
||||
};
|
||||
|
||||
export type SASLMechanism = keyof SASLMechanismOptionsMap;
|
||||
type SASLMechanismOptions<T> = T extends SASLMechanism
|
||||
? { mechanism: T } & SASLMechanismOptionsMap[T]
|
||||
: never;
|
||||
export type SASLOptions = SASLMechanismOptions<SASLMechanism>;
|
||||
|
||||
export interface ProducerConfig {
|
||||
createPartitioner?: ICustomPartitioner;
|
||||
retry?: RetryOptions;
|
||||
@@ -70,24 +100,25 @@ export interface PartitionerArgs {
|
||||
}
|
||||
|
||||
export type ICustomPartitioner = () => (args: PartitionerArgs) => number;
|
||||
export type DefaultPartitioner = (args: PartitionerArgs) => number;
|
||||
export type JavaCompatiblePartitioner = (args: PartitionerArgs) => number;
|
||||
export type DefaultPartitioner = ICustomPartitioner;
|
||||
export type JavaCompatiblePartitioner = ICustomPartitioner;
|
||||
|
||||
export let Partitioners: {
|
||||
DefaultPartitioner: DefaultPartitioner;
|
||||
JavaCompatiblePartitioner: JavaCompatiblePartitioner;
|
||||
};
|
||||
|
||||
export interface PartitionMetadata {
|
||||
export type PartitionMetadata = {
|
||||
partitionErrorCode: number;
|
||||
partitionId: number;
|
||||
leader: number;
|
||||
replicas: number[];
|
||||
isr: number[];
|
||||
}
|
||||
offlineReplicas?: number[];
|
||||
};
|
||||
|
||||
export interface IHeaders {
|
||||
[key: string]: Buffer;
|
||||
[key: string]: Buffer | string | undefined;
|
||||
}
|
||||
|
||||
export interface ConsumerConfig {
|
||||
@@ -101,15 +132,16 @@ export interface ConsumerConfig {
|
||||
minBytes?: number;
|
||||
maxBytes?: number;
|
||||
maxWaitTimeInMs?: number;
|
||||
retry?: RetryOptions;
|
||||
retry?: RetryOptions & {
|
||||
restartOnFailure?: (err: Error) => Promise<boolean>;
|
||||
};
|
||||
allowAutoTopicCreation?: boolean;
|
||||
maxInFlightRequests?: number;
|
||||
readUncommitted?: boolean;
|
||||
rackId?: string;
|
||||
}
|
||||
|
||||
export interface PartitionAssigner {
|
||||
new (config: { cluster: Cluster }): Assigner;
|
||||
}
|
||||
export type PartitionAssigner = (config: { cluster: Cluster }) => Assigner;
|
||||
|
||||
export interface CoordinatorMetadata {
|
||||
errorCode: number;
|
||||
@@ -120,7 +152,7 @@ export interface CoordinatorMetadata {
|
||||
};
|
||||
}
|
||||
|
||||
export interface Cluster {
|
||||
export type Cluster = {
|
||||
isConnected(): boolean;
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
@@ -140,46 +172,38 @@ export interface Cluster {
|
||||
}): Promise<CoordinatorMetadata>;
|
||||
defaultOffset(config: { fromBeginning: boolean }): number;
|
||||
fetchTopicsOffset(
|
||||
topics: Array<{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number }>;
|
||||
fromBeginning: boolean;
|
||||
}>,
|
||||
topics: Array<
|
||||
{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number }>;
|
||||
} & XOR<{ fromBeginning: boolean }, { fromTimestamp: number }>
|
||||
>,
|
||||
): Promise<{
|
||||
topic: string;
|
||||
partitions: Array<{ partition: number; offset: string }>;
|
||||
}>;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Assignment {
|
||||
[topic: string]: number[];
|
||||
}
|
||||
export type Assignment = { [topic: string]: number[] };
|
||||
|
||||
export interface GroupMember {
|
||||
memberId: string;
|
||||
memberMetadata: MemberMetadata;
|
||||
}
|
||||
export type GroupMember = { memberId: string; memberMetadata: Buffer };
|
||||
|
||||
export interface GroupMemberAssignment {
|
||||
export type GroupMemberAssignment = {
|
||||
memberId: string;
|
||||
memberAssignment: Buffer;
|
||||
}
|
||||
};
|
||||
|
||||
export interface GroupState {
|
||||
name: string;
|
||||
metadata: Buffer;
|
||||
}
|
||||
export type GroupState = { name: string; metadata: Buffer };
|
||||
|
||||
export interface Assigner {
|
||||
export type Assigner = {
|
||||
name: string;
|
||||
version: number;
|
||||
assign(group: {
|
||||
members: GroupMember[];
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
}): Promise<GroupMemberAssignment[]>;
|
||||
protocol(subscription: { topics: string[]; userData: Buffer }): GroupState;
|
||||
}
|
||||
protocol(subscription: { topics: string[] }): GroupState;
|
||||
};
|
||||
|
||||
export interface RetryOptions {
|
||||
maxRetryTime?: number;
|
||||
@@ -201,12 +225,22 @@ export interface ITopicConfig {
|
||||
configEntries?: object[];
|
||||
}
|
||||
|
||||
export interface ITopicPartitionConfig {
|
||||
topic: string;
|
||||
count: number;
|
||||
assignments?: Array<Array<number>>;
|
||||
}
|
||||
|
||||
export interface ITopicMetadata {
|
||||
name: string;
|
||||
partitions: PartitionMetadata[];
|
||||
}
|
||||
|
||||
export enum ResourceType {
|
||||
/**
|
||||
* @deprecated
|
||||
* Use ConfigResourceTypes or AclResourceTypes
|
||||
*/
|
||||
export enum ResourceTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
TOPIC = 2,
|
||||
@@ -216,10 +250,58 @@ export enum ResourceType {
|
||||
DELEGATION_TOKEN = 6,
|
||||
}
|
||||
|
||||
export enum AclResourceTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
TOPIC = 2,
|
||||
GROUP = 3,
|
||||
CLUSTER = 4,
|
||||
TRANSACTIONAL_ID = 5,
|
||||
DELEGATION_TOKEN = 6,
|
||||
}
|
||||
|
||||
export enum ConfigResourceTypes {
|
||||
UNKNOWN = 0,
|
||||
TOPIC = 2,
|
||||
BROKER = 4,
|
||||
BROKER_LOGGER = 8,
|
||||
}
|
||||
|
||||
export enum AclPermissionTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
DENY = 2,
|
||||
ALLOW = 3,
|
||||
}
|
||||
|
||||
export enum AclOperationTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
ALL = 2,
|
||||
READ = 3,
|
||||
WRITE = 4,
|
||||
CREATE = 5,
|
||||
DELETE = 6,
|
||||
ALTER = 7,
|
||||
DESCRIBE = 8,
|
||||
CLUSTER_ACTION = 9,
|
||||
DESCRIBE_CONFIGS = 10,
|
||||
ALTER_CONFIGS = 11,
|
||||
IDEMPOTENT_WRITE = 12,
|
||||
}
|
||||
|
||||
export enum ResourcePatternTypes {
|
||||
UNKNOWN = 0,
|
||||
ANY = 1,
|
||||
MATCH = 2,
|
||||
LITERAL = 3,
|
||||
PREFIXED = 4,
|
||||
}
|
||||
|
||||
export interface ResourceConfigQuery {
|
||||
type: ResourceType;
|
||||
type: ResourceTypes | ConfigResourceTypes;
|
||||
name: string;
|
||||
configNames: string[];
|
||||
configNames?: string[];
|
||||
}
|
||||
|
||||
export interface ConfigEntries {
|
||||
@@ -243,26 +325,26 @@ export interface DescribeConfigResponse {
|
||||
errorCode: number;
|
||||
errorMessage: string;
|
||||
resourceName: string;
|
||||
resourceType: ResourceType;
|
||||
resourceType: ResourceTypes | ConfigResourceTypes;
|
||||
}[];
|
||||
throttleTime: number;
|
||||
}
|
||||
|
||||
export interface IResourceConfig {
|
||||
type: ResourceType;
|
||||
type: ResourceTypes | ConfigResourceTypes;
|
||||
name: string;
|
||||
configEntries: { name: string; value: string }[];
|
||||
}
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
||||
export interface AdminEvents {
|
||||
export type AdminEvents = {
|
||||
CONNECT: 'admin.connect';
|
||||
DISCONNECT: 'admin.disconnect';
|
||||
REQUEST: 'admin.network.request';
|
||||
REQUEST_TIMEOUT: 'admin.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'admin.network.request_queue_size';
|
||||
}
|
||||
};
|
||||
|
||||
export interface InstrumentationEvent<T> {
|
||||
id: string;
|
||||
@@ -271,6 +353,8 @@ export interface InstrumentationEvent<T> {
|
||||
payload: T;
|
||||
}
|
||||
|
||||
export type RemoveInstrumentationEventListener<T> = () => void;
|
||||
|
||||
export type ConnectEvent = InstrumentationEvent<null>;
|
||||
export type DisconnectEvent = InstrumentationEvent<null>;
|
||||
export type RequestEvent = InstrumentationEvent<{
|
||||
@@ -308,9 +392,69 @@ export interface SeekEntry {
|
||||
offset: string;
|
||||
}
|
||||
|
||||
export interface Admin {
|
||||
export interface Acl {
|
||||
principal: string;
|
||||
host: string;
|
||||
operation: AclOperationTypes;
|
||||
permissionType: AclPermissionTypes;
|
||||
}
|
||||
|
||||
export interface AclResource {
|
||||
resourceType: AclResourceTypes;
|
||||
resourceName: string;
|
||||
resourcePatternType: ResourcePatternTypes;
|
||||
}
|
||||
|
||||
export type AclEntry = Acl & AclResource;
|
||||
|
||||
export type DescribeAclResource = AclResource & {
|
||||
acl: Acl[];
|
||||
};
|
||||
|
||||
export interface DescribeAclResponse {
|
||||
throttleTime: number;
|
||||
errorCode: number;
|
||||
errorMessage?: string;
|
||||
resources: DescribeAclResource[];
|
||||
}
|
||||
|
||||
export interface AclFilter {
|
||||
resourceType: AclResourceTypes;
|
||||
resourceName?: string;
|
||||
resourcePatternType: ResourcePatternTypes;
|
||||
principal?: string;
|
||||
host?: string;
|
||||
operation: AclOperationTypes;
|
||||
permissionType: AclPermissionTypes;
|
||||
}
|
||||
|
||||
export interface MatchingAcl {
|
||||
errorCode: number;
|
||||
errorMessage?: string;
|
||||
resourceType: AclResourceTypes;
|
||||
resourceName: string;
|
||||
resourcePatternType: ResourcePatternTypes;
|
||||
principal: string;
|
||||
host: string;
|
||||
operation: AclOperationTypes;
|
||||
permissionType: AclPermissionTypes;
|
||||
}
|
||||
|
||||
export interface DeleteAclFilterResponses {
|
||||
errorCode: number;
|
||||
errorMessage?: string;
|
||||
matchingAcls: MatchingAcl[];
|
||||
}
|
||||
|
||||
export interface DeleteAclResponse {
|
||||
throttleTime: number;
|
||||
filterResponses: DeleteAclFilterResponses[];
|
||||
}
|
||||
|
||||
export type Admin = {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
listTopics(): Promise<string[]>;
|
||||
createTopics(options: {
|
||||
validateOnly?: boolean;
|
||||
waitForLeaders?: boolean;
|
||||
@@ -318,20 +462,31 @@ export interface Admin {
|
||||
topics: ITopicConfig[];
|
||||
}): Promise<boolean>;
|
||||
deleteTopics(options: { topics: string[]; timeout?: number }): Promise<void>;
|
||||
fetchTopicMetadata(options: {
|
||||
createPartitions(options: {
|
||||
validateOnly?: boolean;
|
||||
timeout?: number;
|
||||
topicPartitions: ITopicPartitionConfig[];
|
||||
}): Promise<boolean>;
|
||||
fetchTopicMetadata(options?: {
|
||||
topics: string[];
|
||||
}): Promise<{ topics: Array<ITopicMetadata> }>;
|
||||
fetchOffsets(options: {
|
||||
groupId: string;
|
||||
topic: string;
|
||||
}): Promise<
|
||||
Array<{ partition: number; offset: string; metadata: string | null }>
|
||||
>;
|
||||
resolveOffsets?: boolean;
|
||||
}): Promise<Array<SeekEntry & { metadata: string | null }>>;
|
||||
fetchTopicOffsets(
|
||||
topic: string,
|
||||
): Promise<
|
||||
Array<{ partition: number; offset: string; high: string; low: string }>
|
||||
>;
|
||||
): Promise<Array<SeekEntry & { high: string; low: string }>>;
|
||||
fetchTopicOffsetsByTimestamp(
|
||||
topic: string,
|
||||
timestamp?: number,
|
||||
): Promise<Array<SeekEntry>>;
|
||||
describeCluster(): Promise<{
|
||||
brokers: Array<{ nodeId: number; host: string; port: number }>;
|
||||
controller: number | null;
|
||||
clusterId: string;
|
||||
}>;
|
||||
setOffsets(options: {
|
||||
groupId: string;
|
||||
topic: string;
|
||||
@@ -350,29 +505,42 @@ export interface Admin {
|
||||
validateOnly: boolean;
|
||||
resources: IResourceConfig[];
|
||||
}): Promise<any>;
|
||||
listGroups(): Promise<{ groups: GroupOverview[] }>;
|
||||
deleteGroups(groupIds: string[]): Promise<DeleteGroupsResult[]>;
|
||||
describeGroups(groupIds: string[]): Promise<GroupDescriptions>;
|
||||
describeAcls(options: AclFilter): Promise<DescribeAclResponse>;
|
||||
deleteAcls(options: { filters: AclFilter[] }): Promise<DeleteAclResponse>;
|
||||
createAcls(options: { acl: AclEntry[] }): Promise<boolean>;
|
||||
deleteTopicRecords(options: {
|
||||
topic: string;
|
||||
partitions: SeekEntry[];
|
||||
}): Promise<void>;
|
||||
logger(): Logger;
|
||||
on(eventName: ValueOf<AdminEvents>, listener: (...args: any[]) => void): void;
|
||||
on(
|
||||
eventName: ValueOf<AdminEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
events: AdminEvents;
|
||||
}
|
||||
};
|
||||
|
||||
export let PartitionAssigners: { roundRobin: PartitionAssigner };
|
||||
|
||||
export interface ISerializer<T> {
|
||||
encode(value: T): Buffer;
|
||||
decode(buffer: Buffer): T;
|
||||
decode(buffer: Buffer): T | null;
|
||||
}
|
||||
|
||||
export interface MemberMetadata {
|
||||
export type MemberMetadata = {
|
||||
version: number;
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
}
|
||||
};
|
||||
|
||||
export interface MemberAssignment {
|
||||
export type MemberAssignment = {
|
||||
version: number;
|
||||
assignment: Assignment;
|
||||
userData: Buffer;
|
||||
}
|
||||
};
|
||||
|
||||
export let AssignerProtocol: {
|
||||
MemberMetadata: ISerializer<MemberMetadata>;
|
||||
@@ -400,11 +568,16 @@ export interface LoggerEntryContent {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type Logger = (entry: LogEntry) => void;
|
||||
export type logCreator = (logLevel: logLevel) => (entry: LogEntry) => void;
|
||||
|
||||
export type logCreator = (logLevel: string) => (entry: LogEntry) => void;
|
||||
export type Logger = {
|
||||
info: (message: string, extra?: object) => void;
|
||||
error: (message: string, extra?: object) => void;
|
||||
warn: (message: string, extra?: object) => void;
|
||||
debug: (message: string, extra?: object) => void;
|
||||
};
|
||||
|
||||
export interface Broker {
|
||||
export type Broker = {
|
||||
isConnected(): boolean;
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
@@ -414,7 +587,12 @@ export interface Broker {
|
||||
metadata(
|
||||
topics: string[],
|
||||
): Promise<{
|
||||
brokers: Array<{ nodeId: number; host: string; port: number }>;
|
||||
brokers: Array<{
|
||||
nodeId: number;
|
||||
host: string;
|
||||
port: number;
|
||||
rack?: string;
|
||||
}>;
|
||||
topicMetadata: Array<{
|
||||
topicErrorCode: number;
|
||||
topic: number;
|
||||
@@ -431,17 +609,33 @@ export interface Broker {
|
||||
partitions: Array<{ partition: number; offset: string }>;
|
||||
}>;
|
||||
}): Promise<any>;
|
||||
}
|
||||
fetch(request: {
|
||||
replicaId?: number;
|
||||
isolationLevel?: number;
|
||||
maxWaitTime?: number;
|
||||
minBytes?: number;
|
||||
maxBytes?: number;
|
||||
topics: Array<{
|
||||
topic: string;
|
||||
partitions: Array<{
|
||||
partition: number;
|
||||
fetchOffset: string;
|
||||
maxBytes: number;
|
||||
}>;
|
||||
}>;
|
||||
rackId?: string;
|
||||
}): Promise<any>;
|
||||
};
|
||||
|
||||
export interface KafkaMessage {
|
||||
export type KafkaMessage = {
|
||||
key: Buffer;
|
||||
value: Buffer;
|
||||
value: Buffer | null;
|
||||
timestamp: string;
|
||||
size: number;
|
||||
attributes: number;
|
||||
offset: string;
|
||||
headers?: IHeaders;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ProducerRecord {
|
||||
topic: string;
|
||||
@@ -451,13 +645,16 @@ export interface ProducerRecord {
|
||||
compression?: CompressionTypes;
|
||||
}
|
||||
|
||||
export interface RecordMetadata {
|
||||
export type RecordMetadata = {
|
||||
topicName: string;
|
||||
partition: number;
|
||||
errorCode: number;
|
||||
offset: string;
|
||||
timestamp: string;
|
||||
}
|
||||
offset?: string;
|
||||
timestamp?: string;
|
||||
baseOffset?: string;
|
||||
logAppendTime?: string;
|
||||
logStartOffset?: string;
|
||||
};
|
||||
|
||||
export interface TopicMessages {
|
||||
topic: string;
|
||||
@@ -465,10 +662,10 @@ export interface TopicMessages {
|
||||
}
|
||||
|
||||
export interface ProducerBatch {
|
||||
acks: number;
|
||||
timeout: number;
|
||||
compression: CompressionTypes;
|
||||
topicMessages: TopicMessages[];
|
||||
acks?: number;
|
||||
timeout?: number;
|
||||
compression?: CompressionTypes;
|
||||
topicMessages?: TopicMessages[];
|
||||
}
|
||||
|
||||
export interface PartitionOffset {
|
||||
@@ -485,18 +682,18 @@ export interface Offsets {
|
||||
topics: TopicOffsets[];
|
||||
}
|
||||
|
||||
interface Sender {
|
||||
type Sender = {
|
||||
send(record: ProducerRecord): Promise<RecordMetadata[]>;
|
||||
sendBatch(batch: ProducerBatch): Promise<RecordMetadata[]>;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ProducerEvents {
|
||||
export type ProducerEvents = {
|
||||
CONNECT: 'producer.connect';
|
||||
DISCONNECT: 'producer.disconnect';
|
||||
REQUEST: 'producer.network.request';
|
||||
REQUEST_TIMEOUT: 'producer.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'producer.network.request_queue_size';
|
||||
}
|
||||
};
|
||||
|
||||
export type Producer = Sender & {
|
||||
connect(): Promise<void>;
|
||||
@@ -506,7 +703,7 @@ export type Producer = Sender & {
|
||||
on(
|
||||
eventName: ValueOf<ProducerEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): void;
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
transaction(): Promise<Transaction>;
|
||||
logger(): Logger;
|
||||
};
|
||||
@@ -518,41 +715,54 @@ export type Transaction = Sender & {
|
||||
isActive(): boolean;
|
||||
};
|
||||
|
||||
export interface ConsumerGroup {
|
||||
export type ConsumerGroup = {
|
||||
groupId: string;
|
||||
generationId: number;
|
||||
memberId: string;
|
||||
coordinator: Broker;
|
||||
}
|
||||
};
|
||||
|
||||
export interface MemberDescription {
|
||||
export type MemberDescription = {
|
||||
clientHost: string;
|
||||
clientId: string;
|
||||
memberId: string;
|
||||
memberAssignment: Buffer;
|
||||
memberMetadata: Buffer;
|
||||
}
|
||||
};
|
||||
|
||||
export interface GroupDescription {
|
||||
// See https://github.com/apache/kafka/blob/2.4.0/clients/src/main/java/org/apache/kafka/common/ConsumerGroupState.java#L25
|
||||
export type ConsumerGroupState =
|
||||
| 'Unknown'
|
||||
| 'PreparingRebalance'
|
||||
| 'CompletingRebalance'
|
||||
| 'Stable'
|
||||
| 'Dead'
|
||||
| 'Empty';
|
||||
|
||||
export type GroupDescription = {
|
||||
groupId: string;
|
||||
members: MemberDescription[];
|
||||
protocol: string;
|
||||
protocolType: string;
|
||||
state: string;
|
||||
}
|
||||
state: ConsumerGroupState;
|
||||
};
|
||||
|
||||
export interface TopicPartitions {
|
||||
topic: string;
|
||||
partitions: number[];
|
||||
}
|
||||
export interface TopicPartitionOffsetAndMedata {
|
||||
export type GroupDescriptions = {
|
||||
groups: GroupDescription[];
|
||||
};
|
||||
|
||||
export type TopicPartitions = { topic: string; partitions: number[] };
|
||||
export type TopicPartitionOffsetAndMetadata = {
|
||||
topic: string;
|
||||
partition: number;
|
||||
offset: string;
|
||||
metadata?: string | null;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Batch {
|
||||
// TODO: Remove with 2.x
|
||||
export type TopicPartitionOffsetAndMedata = TopicPartitionOffsetAndMetadata;
|
||||
|
||||
export type Batch = {
|
||||
topic: string;
|
||||
partition: number;
|
||||
highWatermark: string;
|
||||
@@ -562,12 +772,24 @@ export interface Batch {
|
||||
lastOffset(): string;
|
||||
offsetLag(): string;
|
||||
offsetLagLow(): string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ConsumerEvents {
|
||||
export type GroupOverview = {
|
||||
groupId: string;
|
||||
protocolType: string;
|
||||
};
|
||||
|
||||
export type DeleteGroupsResult = {
|
||||
groupId: string;
|
||||
errorCode?: number;
|
||||
error?: KafkaJSProtocolError;
|
||||
};
|
||||
|
||||
export type ConsumerEvents = {
|
||||
HEARTBEAT: 'consumer.heartbeat';
|
||||
COMMIT_OFFSETS: 'consumer.commit_offsets';
|
||||
GROUP_JOIN: 'consumer.group_join';
|
||||
FETCH_START: 'consumer.fetch_start';
|
||||
FETCH: 'consumer.fetch';
|
||||
START_BATCH_PROCESS: 'consumer.start_batch_process';
|
||||
END_BATCH_PROCESS: 'consumer.end_batch_process';
|
||||
@@ -575,10 +797,11 @@ export interface ConsumerEvents {
|
||||
DISCONNECT: 'consumer.disconnect';
|
||||
STOP: 'consumer.stop';
|
||||
CRASH: 'consumer.crash';
|
||||
RECEIVED_UNSUBSCRIBED_TOPICS: 'consumer.received_unsubscribed_topics';
|
||||
REQUEST: 'consumer.network.request';
|
||||
REQUEST_TIMEOUT: 'consumer.network.request_timeout';
|
||||
REQUEST_QUEUE_SIZE: 'consumer.network.request_queue_size';
|
||||
}
|
||||
};
|
||||
export type ConsumerHeartbeatEvent = InstrumentationEvent<{
|
||||
groupId: string;
|
||||
memberId: string;
|
||||
@@ -622,15 +845,22 @@ interface IBatchProcessEvent {
|
||||
firstOffset: string;
|
||||
lastOffset: string;
|
||||
}
|
||||
export type ConsumerStartBatchProcessEvent = InstrumentationEvent<
|
||||
IBatchProcessEvent
|
||||
>;
|
||||
export type ConsumerStartBatchProcessEvent = InstrumentationEvent<IBatchProcessEvent>;
|
||||
export type ConsumerEndBatchProcessEvent = InstrumentationEvent<
|
||||
IBatchProcessEvent & { duration: number }
|
||||
>;
|
||||
export type ConsumerCrashEvent = InstrumentationEvent<{
|
||||
error: Error;
|
||||
groupId: string;
|
||||
restart: boolean;
|
||||
}>;
|
||||
export type ConsumerReceivedUnsubcribedTopicsEvent = InstrumentationEvent<{
|
||||
groupId: string;
|
||||
generationId: number;
|
||||
memberId: string;
|
||||
assignedTopics: string[];
|
||||
topicsSubscribed: string[];
|
||||
topicsNotSubscribed: string[];
|
||||
}>;
|
||||
|
||||
export interface OffsetsByTopicPartition {
|
||||
@@ -648,7 +878,7 @@ export interface EachBatchPayload {
|
||||
resolveOffset(offset: string): void;
|
||||
heartbeat(): Promise<void>;
|
||||
commitOffsetsIfNecessary(offsets?: Offsets): Promise<void>;
|
||||
uncommittedOffsets(): Promise<OffsetsByTopicPartition>;
|
||||
uncommittedOffsets(): OffsetsByTopicPartition;
|
||||
isRunning(): boolean;
|
||||
isStale(): boolean;
|
||||
}
|
||||
@@ -665,25 +895,29 @@ export type ConsumerEachMessagePayload = EachMessagePayload;
|
||||
*/
|
||||
export type ConsumerEachBatchPayload = EachBatchPayload;
|
||||
|
||||
export interface Consumer {
|
||||
export type ConsumerRunConfig = {
|
||||
autoCommit?: boolean;
|
||||
autoCommitInterval?: number | null;
|
||||
autoCommitThreshold?: number | null;
|
||||
eachBatchAutoResolve?: boolean;
|
||||
partitionsConsumedConcurrently?: number;
|
||||
eachBatch?: (payload: EachBatchPayload) => Promise<void>;
|
||||
eachMessage?: (payload: EachMessagePayload) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ConsumerSubscribeTopic = {
|
||||
topic: string | RegExp;
|
||||
fromBeginning?: boolean;
|
||||
};
|
||||
|
||||
export type Consumer = {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
subscribe(topic: {
|
||||
topic: string | RegExp;
|
||||
fromBeginning?: boolean;
|
||||
}): Promise<void>;
|
||||
subscribe(topic: ConsumerSubscribeTopic): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
run(config?: {
|
||||
autoCommit?: boolean;
|
||||
autoCommitInterval?: number | null;
|
||||
autoCommitThreshold?: number | null;
|
||||
eachBatchAutoResolve?: boolean;
|
||||
partitionsConsumedConcurrently?: number;
|
||||
eachBatch?: (payload: EachBatchPayload) => Promise<void>;
|
||||
eachMessage?: (payload: EachMessagePayload) => Promise<void>;
|
||||
}): Promise<void>;
|
||||
run(config?: ConsumerRunConfig): Promise<void>;
|
||||
commitOffsets(
|
||||
topicPartitions: Array<TopicPartitionOffsetAndMedata>,
|
||||
topicPartitions: Array<TopicPartitionOffsetAndMetadata>,
|
||||
): Promise<void>;
|
||||
seek(topicPartition: {
|
||||
topic: string;
|
||||
@@ -692,14 +926,15 @@ export interface Consumer {
|
||||
}): void;
|
||||
describeGroup(): Promise<GroupDescription>;
|
||||
pause(topics: Array<{ topic: string; partitions?: number[] }>): void;
|
||||
paused(): TopicPartitions[];
|
||||
resume(topics: Array<{ topic: string; partitions?: number[] }>): void;
|
||||
on(
|
||||
eventName: ValueOf<ConsumerEvents>,
|
||||
listener: (...args: any[]) => void,
|
||||
): void;
|
||||
): RemoveInstrumentationEventListener<typeof eventName>;
|
||||
logger(): Logger;
|
||||
events: ConsumerEvents;
|
||||
}
|
||||
};
|
||||
|
||||
export enum CompressionTypes {
|
||||
None = 0,
|
||||
@@ -715,3 +950,186 @@ export let CompressionCodecs: {
|
||||
[CompressionTypes.LZ4]: () => any;
|
||||
[CompressionTypes.ZSTD]: () => any;
|
||||
};
|
||||
|
||||
export declare class KafkaJSError extends Error {
|
||||
readonly message: Error['message'];
|
||||
readonly name: string;
|
||||
readonly retriable: boolean;
|
||||
readonly helpUrl?: string;
|
||||
|
||||
constructor(e: Error | string, metadata?: KafkaJSErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSNonRetriableError extends KafkaJSError {
|
||||
constructor(e: Error | string);
|
||||
}
|
||||
|
||||
export declare class KafkaJSProtocolError extends KafkaJSError {
|
||||
readonly code: number;
|
||||
readonly type: string;
|
||||
constructor(e: Error | string);
|
||||
}
|
||||
|
||||
export declare class KafkaJSOffsetOutOfRange extends KafkaJSProtocolError {
|
||||
readonly topic: string;
|
||||
readonly partition: number;
|
||||
constructor(e: Error | string, metadata?: KafkaJSOffsetOutOfRangeMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSNumberOfRetriesExceeded extends KafkaJSNonRetriableError {
|
||||
readonly stack: string;
|
||||
readonly originalError: Error;
|
||||
readonly retryCount: number;
|
||||
readonly retryTime: number;
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSNumberOfRetriesExceededMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSConnectionError extends KafkaJSError {
|
||||
readonly broker: string;
|
||||
constructor(e: Error | string, metadata?: KafkaJSConnectionErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSRequestTimeoutError extends KafkaJSError {
|
||||
readonly broker: string;
|
||||
readonly correlationId: number;
|
||||
readonly createdAt: number;
|
||||
readonly sentAt: number;
|
||||
readonly pendingDuration: number;
|
||||
constructor(e: Error | string, metadata?: KafkaJSRequestTimeoutErrorMetadata);
|
||||
}
|
||||
|
||||
export declare class KafkaJSMetadataNotLoaded extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSTopicMetadataNotLoaded extends KafkaJSMetadataNotLoaded {
|
||||
readonly topic: string;
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSTopicMetadataNotLoadedMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSStaleTopicMetadataAssignment extends KafkaJSError {
|
||||
readonly topic: string;
|
||||
readonly unknownPartitions: number;
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSStaleTopicMetadataAssignmentMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSServerDoesNotSupportApiKey extends KafkaJSNonRetriableError {
|
||||
readonly apiKey: number;
|
||||
readonly apiName: string;
|
||||
constructor(
|
||||
e: Error | string,
|
||||
metadata?: KafkaJSServerDoesNotSupportApiKeyMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
export declare class KafkaJSBrokerNotFound extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSPartialMessageError extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSSASLAuthenticationError extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSGroupCoordinatorNotFound extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSNotImplemented extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSTimeout extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSLockTimeout extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSUnsupportedMagicByteInMessageSet extends KafkaJSError {
|
||||
constructor();
|
||||
}
|
||||
|
||||
export declare class KafkaJSDeleteGroupsError extends KafkaJSError {
|
||||
readonly groups: DeleteGroupsResult[];
|
||||
constructor(e: Error | string, groups?: KafkaJSDeleteGroupsErrorGroups[]);
|
||||
}
|
||||
|
||||
export declare class KafkaJSDeleteTopicRecordsError extends KafkaJSError {
|
||||
constructor(metadata: KafkaJSDeleteTopicRecordsErrorTopic);
|
||||
}
|
||||
|
||||
export interface KafkaJSDeleteGroupsErrorGroups {
|
||||
groupId: string;
|
||||
errorCode: number;
|
||||
error: KafkaJSError;
|
||||
}
|
||||
|
||||
export interface KafkaJSDeleteTopicRecordsErrorTopic {
|
||||
topic: string;
|
||||
partitions: KafkaJSDeleteTopicRecordsErrorPartition[];
|
||||
}
|
||||
|
||||
export interface KafkaJSDeleteTopicRecordsErrorPartition {
|
||||
partition: number;
|
||||
offset: string;
|
||||
error: KafkaJSError;
|
||||
}
|
||||
|
||||
export interface KafkaJSErrorMetadata {
|
||||
retriable?: boolean;
|
||||
topic?: string;
|
||||
partitionId?: number;
|
||||
metadata?: PartitionMetadata;
|
||||
}
|
||||
|
||||
export interface KafkaJSOffsetOutOfRangeMetadata {
|
||||
topic: string;
|
||||
partition: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSNumberOfRetriesExceededMetadata {
|
||||
retryCount: number;
|
||||
retryTime: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSConnectionErrorMetadata {
|
||||
broker?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export interface KafkaJSRequestTimeoutErrorMetadata {
|
||||
broker: string;
|
||||
clientId: string;
|
||||
correlationId: number;
|
||||
createdAt: number;
|
||||
sentAt: number;
|
||||
pendingDuration: number;
|
||||
}
|
||||
|
||||
export interface KafkaJSTopicMetadataNotLoadedMetadata {
|
||||
topic: string;
|
||||
}
|
||||
|
||||
export interface KafkaJSStaleTopicMetadataAssignmentMetadata {
|
||||
topic: string;
|
||||
unknownPartitions: PartitionMetadata[];
|
||||
}
|
||||
|
||||
export interface KafkaJSServerDoesNotSupportApiKeyMetadata {
|
||||
apiKey: number;
|
||||
apiName: string;
|
||||
}
|
||||
|
||||
@@ -9,3 +9,22 @@ export interface RmqUrl {
|
||||
heartbeat?: number;
|
||||
vhost?: string;
|
||||
}
|
||||
|
||||
export interface AmqpConnectionManagerSocketOptions {
|
||||
reconnectTimeInSeconds?: number;
|
||||
heartbeatIntervalInSeconds?: number;
|
||||
findServers?: () => string | string[];
|
||||
connectionOptions?: any;
|
||||
}
|
||||
|
||||
export interface AmqplibQueueOptions {
|
||||
durable?: boolean;
|
||||
autoDelete?: boolean;
|
||||
arguments?: any;
|
||||
messageTtl?: number;
|
||||
expires?: number;
|
||||
deadLetterExchange?: string;
|
||||
deadLetterRoutingKey?: string;
|
||||
maxLength?: number;
|
||||
maxPriority?: number;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './json-socket';
|
||||
export * from './kafka-logger';
|
||||
export * from './kafka-parser';
|
||||
export * from './kafka-round-robin-partition-assigner';
|
||||
export * from './kafka-reply-partition-assigner';
|
||||
|
||||
202
packages/microservices/helpers/kafka-reply-partition-assigner.ts
Normal file
202
packages/microservices/helpers/kafka-reply-partition-assigner.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { isUndefined } from '@nestjs/common/utils/shared.utils';
|
||||
import { ClientKafka } from '../client/client-kafka';
|
||||
import {
|
||||
Cluster,
|
||||
GroupMember,
|
||||
GroupMemberAssignment,
|
||||
GroupState,
|
||||
MemberMetadata,
|
||||
} from '../external/kafka.interface';
|
||||
|
||||
let kafkaPackage: any = {};
|
||||
|
||||
export class KafkaReplyPartitionAssigner {
|
||||
readonly name = 'NestReplyPartitionAssigner';
|
||||
readonly version = 1;
|
||||
|
||||
constructor(
|
||||
private readonly clientKafka: ClientKafka,
|
||||
private readonly config: {
|
||||
cluster: Cluster;
|
||||
},
|
||||
) {
|
||||
kafkaPackage = loadPackage(
|
||||
'kafkajs',
|
||||
KafkaReplyPartitionAssigner.name,
|
||||
() => require('kafkajs'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This process can result in imbalanced assignments
|
||||
* @param {array} members array of members, e.g: [{ memberId: 'test-5f93f5a3' }]
|
||||
* @param {array} topics
|
||||
* @param {Buffer} userData
|
||||
* @returns {array} object partitions per topic per member
|
||||
*/
|
||||
public async assign(group: {
|
||||
members: GroupMember[];
|
||||
topics: string[];
|
||||
}): Promise<GroupMemberAssignment[]> {
|
||||
const assignment = {};
|
||||
const previousAssignment = {};
|
||||
|
||||
const membersCount = group.members.length;
|
||||
const decodedMembers = group.members.map(member =>
|
||||
this.decodeMember(member),
|
||||
);
|
||||
const sortedMemberIds = decodedMembers
|
||||
.map(member => member.memberId)
|
||||
.sort();
|
||||
|
||||
// build the previous assignment and an inverse map of topic > partition > memberId for lookup
|
||||
decodedMembers.forEach(member => {
|
||||
if (
|
||||
!previousAssignment[member.memberId] &&
|
||||
Object.keys(member.previousAssignment).length > 0
|
||||
) {
|
||||
previousAssignment[member.memberId] = member.previousAssignment;
|
||||
}
|
||||
});
|
||||
|
||||
// build a collection of topics and partitions
|
||||
const topicsPartitions = group.topics
|
||||
.map(topic => {
|
||||
const partitionMetadata = this.config.cluster.findTopicPartitionMetadata(
|
||||
topic,
|
||||
);
|
||||
return partitionMetadata.map(m => {
|
||||
return {
|
||||
topic,
|
||||
partitionId: m.partitionId,
|
||||
};
|
||||
});
|
||||
})
|
||||
.reduce((acc, val) => acc.concat(val), []);
|
||||
|
||||
// create the new assignment by populating the members with the first partition of the topics
|
||||
sortedMemberIds.forEach(assignee => {
|
||||
if (!assignment[assignee]) {
|
||||
assignment[assignee] = {};
|
||||
}
|
||||
|
||||
// add topics to each member
|
||||
group.topics.forEach(topic => {
|
||||
if (!assignment[assignee][topic]) {
|
||||
assignment[assignee][topic] = [];
|
||||
}
|
||||
|
||||
// see if the topic and partition belong to a previous assignment
|
||||
if (
|
||||
previousAssignment[assignee] &&
|
||||
!isUndefined(previousAssignment[assignee][topic])
|
||||
) {
|
||||
// take the minimum partition since replies will be sent to the minimum partition
|
||||
const firstPartition = previousAssignment[assignee][topic];
|
||||
|
||||
// create the assignment with the first partition
|
||||
assignment[assignee][topic].push(firstPartition);
|
||||
|
||||
// find and remove this topic and partition from the topicPartitions to be assigned later
|
||||
const topicsPartitionsIndex = topicsPartitions.findIndex(
|
||||
topicPartition => {
|
||||
return (
|
||||
topicPartition.topic === topic &&
|
||||
topicPartition.partitionId === firstPartition
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// only continue if we found a partition matching this topic
|
||||
if (topicsPartitionsIndex !== -1) {
|
||||
// remove inline
|
||||
topicsPartitions.splice(topicsPartitionsIndex, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// check for member topics that have a partition length of 0
|
||||
sortedMemberIds.forEach(assignee => {
|
||||
group.topics.forEach(topic => {
|
||||
// only continue if there are no partitions for assignee's topic
|
||||
if (assignment[assignee][topic].length === 0) {
|
||||
// find the first partition for this topic
|
||||
const topicsPartitionsIndex = topicsPartitions.findIndex(
|
||||
topicPartition => {
|
||||
return topicPartition.topic === topic;
|
||||
},
|
||||
);
|
||||
|
||||
if (topicsPartitionsIndex !== -1) {
|
||||
// find and set the topic partition
|
||||
const partition =
|
||||
topicsPartitions[topicsPartitionsIndex].partitionId;
|
||||
|
||||
assignment[assignee][topic].push(partition);
|
||||
|
||||
// remove this partition from the topics partitions collection
|
||||
topicsPartitions.splice(topicsPartitionsIndex, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// then balance out the rest of the topic partitions across the members
|
||||
const insertAssignmentsByTopic = (topicPartition, i) => {
|
||||
const assignee = sortedMemberIds[i % membersCount];
|
||||
|
||||
assignment[assignee][topicPartition.topic].push(
|
||||
topicPartition.partitionId,
|
||||
);
|
||||
};
|
||||
|
||||
// build the assignments
|
||||
topicsPartitions.forEach(insertAssignmentsByTopic);
|
||||
|
||||
// encode the end result
|
||||
return Object.keys(assignment).map(memberId => ({
|
||||
memberId,
|
||||
memberAssignment: kafkaPackage.AssignerProtocol.MemberAssignment.encode({
|
||||
version: this.version,
|
||||
assignment: assignment[memberId],
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
public protocol(subscription: {
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
}): GroupState {
|
||||
const stringifiedUserData = JSON.stringify({
|
||||
previousAssignment: this.getPreviousAssignment(),
|
||||
});
|
||||
subscription.userData = Buffer.from(stringifiedUserData);
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
metadata: kafkaPackage.AssignerProtocol.MemberMetadata.encode({
|
||||
version: this.version,
|
||||
topics: subscription.topics,
|
||||
userData: subscription.userData,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
public getPreviousAssignment() {
|
||||
return this.clientKafka.getConsumerAssignments();
|
||||
}
|
||||
|
||||
public decodeMember(member: GroupMember) {
|
||||
const memberMetadata = kafkaPackage.AssignerProtocol.MemberMetadata.decode(
|
||||
member.memberMetadata,
|
||||
) as MemberMetadata;
|
||||
const memberUserData = JSON.parse(memberMetadata.userData.toString());
|
||||
|
||||
return {
|
||||
memberId: member.memberId,
|
||||
previousAssignment: memberUserData.previousAssignment,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import {
|
||||
Cluster,
|
||||
GroupMember,
|
||||
GroupMemberAssignment,
|
||||
GroupState,
|
||||
MemberMetadata,
|
||||
} from '../external/kafka.interface';
|
||||
|
||||
let kafkaPackage: any = {};
|
||||
|
||||
const time = process.hrtime();
|
||||
|
||||
export class KafkaRoundRobinPartitionAssigner {
|
||||
readonly name = 'RoundRobinByTime';
|
||||
readonly version = 1;
|
||||
|
||||
constructor(private readonly config: { cluster: Cluster }) {
|
||||
kafkaPackage = loadPackage(
|
||||
'kafkajs',
|
||||
KafkaRoundRobinPartitionAssigner.name,
|
||||
() => require('kafkajs'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This process can result in imbalanced assignments
|
||||
* @param {array} members array of members, e.g: [{ memberId: 'test-5f93f5a3' }]
|
||||
* @param {array} topics
|
||||
* @param {Buffer} userData
|
||||
* @returns {array} object partitions per topic per member
|
||||
*/
|
||||
public async assign(group: {
|
||||
members: GroupMember[];
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
}): Promise<GroupMemberAssignment[]> {
|
||||
const membersCount = group.members.length;
|
||||
const assignment = {};
|
||||
|
||||
const sortedMembers = group.members
|
||||
.map(member => this.mapToTimeAndMemberId(member))
|
||||
.sort((a, b) => this.sortByTime(a, b))
|
||||
.map(member => member.memberId);
|
||||
|
||||
sortedMembers.forEach(memberId => {
|
||||
assignment[memberId] = {};
|
||||
});
|
||||
|
||||
const insertAssignmentsByTopic = (topic: string) => {
|
||||
const partitionMetadata = this.config.cluster.findTopicPartitionMetadata(
|
||||
topic,
|
||||
);
|
||||
const partitions = partitionMetadata.map(m => m.partitionId);
|
||||
sortedMembers.forEach((memberId, i) => {
|
||||
if (!assignment[memberId][topic]) {
|
||||
assignment[memberId][topic] = [];
|
||||
}
|
||||
|
||||
assignment[memberId][topic].push(
|
||||
...partitions.filter(id => id % membersCount === i),
|
||||
);
|
||||
});
|
||||
};
|
||||
group.topics.forEach(insertAssignmentsByTopic);
|
||||
|
||||
return Object.keys(assignment).map(memberId => ({
|
||||
memberId,
|
||||
memberAssignment: kafkaPackage.AssignerProtocol.MemberAssignment.encode({
|
||||
version: this.version,
|
||||
assignment: assignment[memberId],
|
||||
userData: group.userData,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
public protocol(subscription: {
|
||||
topics: string[];
|
||||
userData: Buffer;
|
||||
}): GroupState {
|
||||
const stringifiedTimeObject = JSON.stringify({
|
||||
time: this.getTime(),
|
||||
});
|
||||
subscription.userData = Buffer.from(stringifiedTimeObject);
|
||||
return {
|
||||
name: this.name,
|
||||
metadata: kafkaPackage.AssignerProtocol.MemberMetadata.encode({
|
||||
version: this.version,
|
||||
topics: subscription.topics,
|
||||
userData: subscription.userData,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
public getTime(): [number, number] {
|
||||
return time;
|
||||
}
|
||||
|
||||
public mapToTimeAndMemberId(member: GroupMember) {
|
||||
const memberMetadata = kafkaPackage.AssignerProtocol.MemberMetadata.decode(
|
||||
member.memberMetadata,
|
||||
) as MemberMetadata;
|
||||
const memberUserData = JSON.parse(memberMetadata.userData.toString());
|
||||
|
||||
return {
|
||||
memberId: member.memberId,
|
||||
time: memberUserData.time,
|
||||
};
|
||||
}
|
||||
|
||||
public sortByTime(a: Record<'time', number[]>, b: Record<'time', number[]>) {
|
||||
// if seconds are equal sort by nanoseconds
|
||||
if (a.time[0] === b.time[0]) {
|
||||
return a.time[1] - b.time[1];
|
||||
}
|
||||
// sort by seconds
|
||||
return a.time[0] - b.time[0];
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Transport } from '../enums/transport.enum';
|
||||
import { ChannelOptions } from '../external/grpc-options.interface';
|
||||
import {
|
||||
CompressionTypes,
|
||||
ConsumerConfig,
|
||||
ConsumerRunConfig,
|
||||
ConsumerSubscribeTopic,
|
||||
KafkaConfig,
|
||||
ProducerConfig,
|
||||
} from '../external/kafka-options.interface';
|
||||
ProducerRecord,
|
||||
} from '../external/kafka.interface';
|
||||
import { MqttClientOptions } from '../external/mqtt-options.interface';
|
||||
import { ClientOpts } from '../external/redis.interface';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
import { Server } from '../server/server';
|
||||
import { CustomTransportStrategy } from './custom-transport-strategy.interface';
|
||||
import { Deserializer } from './deserializer.interface';
|
||||
import { Serializer } from './serializer.interface';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
|
||||
export type MicroserviceOptions =
|
||||
| GrpcOptions
|
||||
@@ -25,7 +27,7 @@ export type MicroserviceOptions =
|
||||
| CustomStrategy;
|
||||
|
||||
export interface CustomStrategy {
|
||||
strategy: Server & CustomTransportStrategy;
|
||||
strategy: CustomTransportStrategy;
|
||||
options?: {};
|
||||
}
|
||||
|
||||
@@ -101,13 +103,19 @@ export interface MqttOptions {
|
||||
export interface NatsOptions {
|
||||
transport?: Transport.NATS;
|
||||
options?: {
|
||||
encoding?: string;
|
||||
url?: string;
|
||||
name?: string;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
maxPingOut?: number;
|
||||
maxReconnectAttempts?: number;
|
||||
reconnectTimeWait?: number;
|
||||
reconnectJitter?: number;
|
||||
reconnectJitterTLS?: number;
|
||||
reconnectDelayHandler?: any;
|
||||
servers?: string[];
|
||||
nkey?: any;
|
||||
reconnect?: boolean;
|
||||
pedantic?: boolean;
|
||||
tls?: any;
|
||||
@@ -117,6 +125,18 @@ export interface NatsOptions {
|
||||
userJWT?: string;
|
||||
nonceSigner?: any;
|
||||
userCreds?: any;
|
||||
useOldRequestStyle?: boolean;
|
||||
pingInterval?: number;
|
||||
preserveBuffers?: boolean;
|
||||
waitOnFirstConnect?: boolean;
|
||||
verbose?: boolean;
|
||||
noEcho?: boolean;
|
||||
noRandomize?: boolean;
|
||||
timeout?: number;
|
||||
token?: string;
|
||||
yieldTime?: number;
|
||||
tokenHandler?: any;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,8 +147,8 @@ export interface RmqOptions {
|
||||
queue?: string;
|
||||
prefetchCount?: number;
|
||||
isGlobalPrefetchCount?: boolean;
|
||||
queueOptions?: any;
|
||||
socketOptions?: any;
|
||||
queueOptions?: any; // AmqplibQueueOptions;
|
||||
socketOptions?: any; // AmqpConnectionManagerSocketOptions;
|
||||
noAck?: boolean;
|
||||
serializer?: Serializer;
|
||||
deserializer?: Deserializer;
|
||||
@@ -140,24 +160,13 @@ export interface RmqOptions {
|
||||
export interface KafkaOptions {
|
||||
transport?: Transport.KAFKA;
|
||||
options?: {
|
||||
postfixId?: string;
|
||||
client?: KafkaConfig;
|
||||
consumer?: ConsumerConfig;
|
||||
run?: {
|
||||
autoCommit?: boolean;
|
||||
autoCommitInterval?: number | null;
|
||||
autoCommitThreshold?: number | null;
|
||||
eachBatchAutoResolve?: boolean;
|
||||
partitionsConsumedConcurrently?: number;
|
||||
};
|
||||
subscribe?: {
|
||||
fromBeginning?: boolean;
|
||||
};
|
||||
run?: Omit<ConsumerRunConfig, 'eachBatch' | 'eachMessage'>;
|
||||
subscribe?: Omit<ConsumerSubscribeTopic, 'topic'>;
|
||||
producer?: ProducerConfig;
|
||||
send?: {
|
||||
acks?: number;
|
||||
timeout?: number;
|
||||
compression?: CompressionTypes;
|
||||
};
|
||||
send?: Omit<ProducerRecord, 'topic' | 'messages'>;
|
||||
serializer?: Serializer;
|
||||
deserializer?: Deserializer;
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ export class NestMicroservice
|
||||
!this.isInitialized && (await this.registerModules());
|
||||
|
||||
this.logger.log(MESSAGES.MICROSERVICE_READY);
|
||||
return new Promise(resolve => this.server.listen(resolve));
|
||||
return new Promise<void>(resolve => this.server.listen(resolve));
|
||||
}
|
||||
|
||||
public async close(): Promise<any> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/microservices",
|
||||
"version": "7.5.5",
|
||||
"version": "7.6.7",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@microservices)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -19,15 +19,54 @@
|
||||
"dependencies": {
|
||||
"iterare": "1.2.1",
|
||||
"json-socket": "0.3.0",
|
||||
"tslib": "2.0.3"
|
||||
"tslib": "2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "7.5.5",
|
||||
"@nestjs/core": "7.5.5"
|
||||
"@nestjs/common": "7.6.7",
|
||||
"@nestjs/core": "7.6.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.0.0",
|
||||
"@nestjs/core": "^7.0.0",
|
||||
"@nestjs/websockets": "^7.0.0",
|
||||
"amqp-connection-manager": "*",
|
||||
"amqplib": "*",
|
||||
"cache-manager": "*",
|
||||
"grpc": "*",
|
||||
"kafkajs": "*",
|
||||
"mqtt": "*",
|
||||
"nats": "*",
|
||||
"redis": "*",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/websockets": {
|
||||
"optional": true
|
||||
},
|
||||
"cache-manager": {
|
||||
"optional": true
|
||||
},
|
||||
"grpc": {
|
||||
"optional": true
|
||||
},
|
||||
"kafkajs": {
|
||||
"optional": true
|
||||
},
|
||||
"mqtt": {
|
||||
"optional": true
|
||||
},
|
||||
"nats": {
|
||||
"optional": true
|
||||
},
|
||||
"redis": {
|
||||
"optional": true
|
||||
},
|
||||
"amqplib": {
|
||||
"optional": true
|
||||
},
|
||||
"amqp-connection-manager": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
KafkaMessage,
|
||||
Message,
|
||||
Producer,
|
||||
RecordMetadata,
|
||||
} from '../external/kafka.interface';
|
||||
import { KafkaLogger, KafkaParser } from '../helpers';
|
||||
import {
|
||||
@@ -50,17 +51,16 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
this.getOptionsProp(this.options, 'client') || ({} as KafkaConfig);
|
||||
const consumerOptions =
|
||||
this.getOptionsProp(this.options, 'consumer') || ({} as ConsumerConfig);
|
||||
const postfixId =
|
||||
this.getOptionsProp(this.options, 'postfixId') || '-server';
|
||||
|
||||
this.brokers = clientOptions.brokers || [KAFKA_DEFAULT_BROKER];
|
||||
|
||||
// append a unique id to the clientId and groupId
|
||||
// so they don't collide with a microservices client
|
||||
this.clientId =
|
||||
(clientOptions.clientId || KAFKA_DEFAULT_CLIENT) +
|
||||
(clientOptions.clientIdPostfix || '-server');
|
||||
this.groupId =
|
||||
(consumerOptions.groupId || KAFKA_DEFAULT_GROUP) +
|
||||
(clientOptions.clientIdPostfix || '-server');
|
||||
(clientOptions.clientId || KAFKA_DEFAULT_CLIENT) + postfixId;
|
||||
this.groupId = (consumerOptions.groupId || KAFKA_DEFAULT_GROUP) + postfixId;
|
||||
|
||||
kafkaPackage = this.loadPackage('kafkajs', ServerKafka.name, () =>
|
||||
require('kafkajs'),
|
||||
@@ -75,9 +75,9 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
await this.start(callback);
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.consumer && this.consumer.disconnect();
|
||||
this.producer && this.producer.disconnect();
|
||||
public async close(): Promise<void> {
|
||||
this.consumer && (await this.consumer.disconnect());
|
||||
this.producer && (await this.producer.disconnect());
|
||||
this.consumer = null;
|
||||
this.producer = null;
|
||||
this.client = null;
|
||||
@@ -130,7 +130,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
replyTopic: string,
|
||||
replyPartition: string,
|
||||
correlationId: string,
|
||||
): (data: any) => any {
|
||||
): (data: any) => Promise<RecordMetadata[]> {
|
||||
return (data: any) =>
|
||||
this.sendMessage(data, replyTopic, replyPartition, correlationId);
|
||||
}
|
||||
@@ -184,7 +184,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
replyTopic: string,
|
||||
replyPartition: string,
|
||||
correlationId: string,
|
||||
): void {
|
||||
): Promise<RecordMetadata[]> {
|
||||
const outgoingMessage = this.serializer.serialize(message.response);
|
||||
this.assignReplyPartition(replyPartition, outgoingMessage);
|
||||
this.assignCorrelationIdHeader(correlationId, outgoingMessage);
|
||||
@@ -198,7 +198,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
},
|
||||
this.options.send || {},
|
||||
);
|
||||
this.producer.send(replyMessage);
|
||||
return this.producer.send(replyMessage);
|
||||
}
|
||||
|
||||
public assignIsDisposedHeader(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { isString, isUndefined } from '@nestjs/common/utils/shared.utils';
|
||||
import {
|
||||
isNil,
|
||||
isString,
|
||||
isUndefined,
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
CONNECT_EVENT,
|
||||
@@ -14,13 +18,13 @@ import {
|
||||
} from '../constants';
|
||||
import { RmqContext } from '../ctx-host';
|
||||
import { Transport } from '../enums';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
import { CustomTransportStrategy, RmqOptions } from '../interfaces';
|
||||
import {
|
||||
IncomingRequest,
|
||||
OutgoingResponse,
|
||||
} from '../interfaces/packet.interface';
|
||||
import { Server } from './server';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
|
||||
let rqmPackage: any = {};
|
||||
|
||||
@@ -111,6 +115,9 @@ export class ServerRMQ extends Server implements CustomTransportStrategy {
|
||||
message: Record<string, any>,
|
||||
channel: any,
|
||||
): Promise<void> {
|
||||
if (isNil(message)) {
|
||||
return;
|
||||
}
|
||||
const { content, properties } = message;
|
||||
const rawMessage = JSON.parse(content.toString());
|
||||
const packet = this.deserializer.deserialize(rawMessage);
|
||||
|
||||
@@ -61,14 +61,16 @@ export abstract class Server {
|
||||
|
||||
public send(
|
||||
stream$: Observable<any>,
|
||||
respond: (data: WritePacket) => void,
|
||||
respond: (data: WritePacket) => unknown | Promise<unknown>,
|
||||
): Subscription {
|
||||
let dataBuffer: WritePacket[] = null;
|
||||
const scheduleOnNextTick = (data: WritePacket) => {
|
||||
if (!dataBuffer) {
|
||||
dataBuffer = [data];
|
||||
process.nextTick(() => {
|
||||
dataBuffer.forEach(buffer => respond(buffer));
|
||||
process.nextTick(async () => {
|
||||
for (const item of dataBuffer) {
|
||||
await respond(item);
|
||||
}
|
||||
dataBuffer = null;
|
||||
});
|
||||
} else if (!data.isDisposed) {
|
||||
@@ -95,7 +97,9 @@ export abstract class Server {
|
||||
): Promise<any> {
|
||||
const handler = this.getHandlerByPattern(pattern);
|
||||
if (!handler) {
|
||||
return this.logger.error(NO_EVENT_HANDLER);
|
||||
return this.logger.error(
|
||||
`${NO_EVENT_HANDLER} Event pattern: ${JSON.stringify(pattern)}.`,
|
||||
);
|
||||
}
|
||||
const resultOrStream = await handler(packet.data, context);
|
||||
if (this.isObservable(resultOrStream)) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as sinon from 'sinon';
|
||||
import { ClientKafka } from '../../client/client-kafka';
|
||||
import { NO_MESSAGE_HANDLER } from '../../constants';
|
||||
import { KafkaHeaders } from '../../enums';
|
||||
import { InvalidKafkaClientTopicPartitionException } from '../../errors/invalid-kafka-client-topic-partition.exception';
|
||||
import { InvalidKafkaClientTopicException } from '../../errors/invalid-kafka-client-topic.exception';
|
||||
import {
|
||||
ConsumerGroupJoinEvent,
|
||||
@@ -269,6 +268,7 @@ describe('ClientKafka', () => {
|
||||
|
||||
expect(createClientStub.calledOnce).to.be.true;
|
||||
expect(producerStub.calledOnce).to.be.true;
|
||||
|
||||
expect(consumerStub.calledOnce).to.be.true;
|
||||
|
||||
expect(on.calledOnce).to.be.true;
|
||||
@@ -314,13 +314,19 @@ describe('ClientKafka', () => {
|
||||
memberId: 'member-1',
|
||||
memberAssignment: {
|
||||
'topic-a': [0, 1, 2],
|
||||
'topic-b': [3, 4, 5],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
client['setConsumerAssignments'](consumerAssignments);
|
||||
|
||||
expect(client['consumerAssignments']).to.deep.eq(
|
||||
consumerAssignments.payload.memberAssignment,
|
||||
// consumerAssignments.payload.memberAssignment,
|
||||
{
|
||||
'topic-a': 0,
|
||||
'topic-b': 3,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -493,10 +499,22 @@ describe('ClientKafka', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConsumerAssignments', () => {
|
||||
it('should get consumer assignments', () => {
|
||||
client['consumerAssignments'] = {
|
||||
[replyTopic]: 0,
|
||||
};
|
||||
|
||||
const result = client.getConsumerAssignments();
|
||||
|
||||
expect(result).to.deep.eq(client['consumerAssignments']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReplyTopicPartition', () => {
|
||||
it('should get reply partition', () => {
|
||||
client['consumerAssignments'] = {
|
||||
[replyTopic]: [0],
|
||||
[replyTopic]: 0,
|
||||
};
|
||||
|
||||
const result = client['getReplyTopicPartition'](replyTopic);
|
||||
@@ -504,19 +522,17 @@ describe('ClientKafka', () => {
|
||||
expect(result).to.eq('0');
|
||||
});
|
||||
|
||||
it('should throw error when the topic is being consumed but is not assigned partitions', () => {
|
||||
client['consumerAssignments'] = {
|
||||
[replyTopic]: [],
|
||||
};
|
||||
it('should throw error when the topic is not being consumed', () => {
|
||||
client['consumerAssignments'] = {};
|
||||
|
||||
expect(() => client['getReplyTopicPartition'](replyTopic)).to.throw(
|
||||
InvalidKafkaClientTopicPartitionException,
|
||||
InvalidKafkaClientTopicException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when the topic is not being consumer', () => {
|
||||
it('should throw error when the topic is not being consumed', () => {
|
||||
client['consumerAssignments'] = {
|
||||
[topic]: [],
|
||||
[topic]: undefined,
|
||||
};
|
||||
|
||||
expect(() => client['getReplyTopicPartition'](replyTopic)).to.throw(
|
||||
@@ -551,7 +567,7 @@ describe('ClientKafka', () => {
|
||||
'getReplyTopicPartition',
|
||||
);
|
||||
routingMapSetSpy = sinon.spy((client as any).routingMap, 'set');
|
||||
sendSpy = sinon.spy();
|
||||
sendSpy = sinon.spy(() => Promise.resolve());
|
||||
|
||||
// stub
|
||||
assignPacketIdStub = sinon
|
||||
@@ -568,7 +584,7 @@ describe('ClientKafka', () => {
|
||||
|
||||
// set
|
||||
client['consumerAssignments'] = {
|
||||
[replyTopic]: [parseFloat(replyPartition)],
|
||||
[replyTopic]: parseFloat(replyPartition),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import * as Kafka from 'kafkajs';
|
||||
import { KafkaReplyPartitionAssigner } from '../../helpers/kafka-reply-partition-assigner';
|
||||
import { ClientKafka } from '../../client/client-kafka';
|
||||
|
||||
describe('kafka reply partition assigner', () => {
|
||||
let cluster, topics, metadata, assigner, client;
|
||||
|
||||
let getConsumerAssignments: sinon.SinonSpy;
|
||||
let getPreviousAssignment: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
metadata = {};
|
||||
cluster = { findTopicPartitionMetadata: topic => metadata[topic] };
|
||||
client = new ClientKafka({});
|
||||
assigner = new KafkaReplyPartitionAssigner(client, { cluster });
|
||||
topics = ['topic-A', 'topic-B'];
|
||||
|
||||
getConsumerAssignments = sinon.spy(client, 'getConsumerAssignments');
|
||||
getPreviousAssignment = sinon.spy(assigner, 'getPreviousAssignment');
|
||||
|
||||
// reset previous assignments
|
||||
(client as any).consumerAssignments = {};
|
||||
});
|
||||
|
||||
describe('assign', () => {
|
||||
it('assign all partitions evenly', async () => {
|
||||
metadata['topic-A'] = Array(14)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
metadata['topic-B'] = Array(5)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
const members = [
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const assignment = await assigner.assign({ members, topics });
|
||||
|
||||
expect(assignment).to.deep.equal([
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [0, 4, 8, 12],
|
||||
'topic-B': [0],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [1, 5, 9, 13],
|
||||
'topic-B': [1],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [2, 6, 10],
|
||||
'topic-B': [2, 4],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [3, 7, 11],
|
||||
'topic-B': [3],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('re-assign', () => {
|
||||
it('assign all partitions evenly', async () => {
|
||||
metadata['topic-A'] = Array(11)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
metadata['topic-B'] = Array(7)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
const members = [
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {
|
||||
'topic-A': 0,
|
||||
'topic-B': 0,
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {
|
||||
'topic-A': 1,
|
||||
'topic-B': 1,
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {
|
||||
'topic-A': 2,
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: {},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const assignment = await assigner.assign({ members, topics });
|
||||
|
||||
expect(assignment).to.deep.equal([
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [1, 4, 8],
|
||||
'topic-B': [1, 5],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [3, 5, 9],
|
||||
'topic-B': [2, 6],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [0, 6, 10],
|
||||
'topic-B': [0],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [2, 7],
|
||||
'topic-B': [3, 4],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('protocol', () => {
|
||||
it('returns the assigner name and metadata', () => {
|
||||
// set previous assignments
|
||||
(client as any).consumerAssignments = {
|
||||
'topic-A': 0,
|
||||
'topic-B': 1,
|
||||
};
|
||||
|
||||
const protocol = assigner.protocol({ topics });
|
||||
|
||||
expect(getPreviousAssignment.calledOnce).to.be.true;
|
||||
expect(getConsumerAssignments.calledOnce).to.be.true;
|
||||
|
||||
expect(protocol).to.deep.equal({
|
||||
name: assigner.name,
|
||||
metadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics,
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
previousAssignment: (client as any).consumerAssignments,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import { expect } from 'chai';
|
||||
import * as Kafka from 'kafkajs';
|
||||
import { KafkaRoundRobinPartitionAssigner } from '../../helpers/kafka-round-robin-partition-assigner';
|
||||
|
||||
describe('kafka round robin by time', () => {
|
||||
let cluster, topics, metadata, assigner;
|
||||
|
||||
beforeEach(() => {
|
||||
metadata = {};
|
||||
cluster = { findTopicPartitionMetadata: topic => metadata[topic] };
|
||||
assigner = new KafkaRoundRobinPartitionAssigner({ cluster });
|
||||
topics = ['topic-A', 'topic-B'];
|
||||
});
|
||||
|
||||
describe('assign', () => {
|
||||
it('assign all partitions evenly', async () => {
|
||||
metadata['topic-A'] = Array(14)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
metadata['topic-B'] = Array(5)
|
||||
.fill(1)
|
||||
.map((_, i) => ({ partitionId: i }));
|
||||
|
||||
const members = [
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
time: [0, 0], // process.hrtime()
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
time: [0, 1], // process.hrtime()
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
time: [1, 1], // process.hrtime()
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberMetadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics: ['topic-A', 'topic-B'],
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
time: [2, 0], // process.hrtime()
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const assignment = await assigner.assign({ members, topics });
|
||||
|
||||
expect(assignment).to.deep.equal([
|
||||
{
|
||||
memberId: 'member-3',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [0, 4, 8, 12],
|
||||
'topic-B': [0, 4],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-1',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [1, 5, 9, 13],
|
||||
'topic-B': [1],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-4',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [2, 6, 10],
|
||||
'topic-B': [2],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
memberAssignment: Kafka.AssignerProtocol.MemberAssignment.encode({
|
||||
version: assigner.version,
|
||||
assignment: {
|
||||
'topic-A': [3, 7, 11],
|
||||
'topic-B': [3],
|
||||
},
|
||||
userData: Buffer.alloc(0),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('protocol', () => {
|
||||
it('returns the assigner name and metadata', () => {
|
||||
expect(assigner.protocol({ topics })).to.deep.equal({
|
||||
name: assigner.name,
|
||||
metadata: Kafka.AssignerProtocol.MemberMetadata.encode({
|
||||
version: assigner.version,
|
||||
topics,
|
||||
userData: Buffer.from(
|
||||
JSON.stringify({
|
||||
time: assigner.getTime(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,7 @@ describe('JsonSocket connection', () => {
|
||||
new Promise(callback => {
|
||||
clientSocket.sendMessage({ type: 'ping' }, callback);
|
||||
}),
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
clientSocket.on(MESSAGE_EVENT, (message: string) => {
|
||||
expect(message).to.deep.equal({ type: 'pong' });
|
||||
callback();
|
||||
@@ -53,16 +53,16 @@ describe('JsonSocket connection', () => {
|
||||
expect(clientSocket['isClosed']).to.equal(false);
|
||||
expect(serverSocket['isClosed']).to.equal(false);
|
||||
Promise.all([
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
clientSocket.sendMessage(longPayload, callback);
|
||||
}),
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
clientSocket.on(MESSAGE_EVENT, (message: { type: 'pong' }) => {
|
||||
expect(message).to.deep.equal({ type: 'pong' });
|
||||
callback();
|
||||
});
|
||||
}),
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
serverSocket.on(MESSAGE_EVENT, (message: { type: 'pong' }) => {
|
||||
expect(message).to.deep.equal(longPayload);
|
||||
serverSocket.sendMessage({ type: 'pong' }, callback);
|
||||
@@ -85,7 +85,7 @@ describe('JsonSocket connection', () => {
|
||||
return done(err);
|
||||
}
|
||||
Promise.all([
|
||||
new Promise(callback =>
|
||||
new Promise<void>(callback =>
|
||||
Promise.all(
|
||||
helpers
|
||||
.range(1, 100)
|
||||
@@ -97,7 +97,7 @@ describe('JsonSocket connection', () => {
|
||||
),
|
||||
).then(_ => callback()),
|
||||
),
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
let lastNumber = 0;
|
||||
serverSocket.on(MESSAGE_EVENT, (message: { number: number }) => {
|
||||
expect(message.number).to.deep.equal(lastNumber + 1);
|
||||
@@ -128,7 +128,7 @@ describe('JsonSocket connection', () => {
|
||||
})
|
||||
.then(
|
||||
() =>
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
expect(clientSocket['isClosed']).to.equal(true);
|
||||
expect(serverSocket['isClosed']).to.equal(true);
|
||||
callback();
|
||||
@@ -154,7 +154,7 @@ describe('JsonSocket connection', () => {
|
||||
})
|
||||
.then(
|
||||
() =>
|
||||
new Promise(callback => {
|
||||
new Promise<void>(callback => {
|
||||
expect(clientSocket['isClosed']).to.equal(true);
|
||||
expect(serverSocket['isClosed']).to.equal(true);
|
||||
callback();
|
||||
|
||||
@@ -135,8 +135,8 @@ describe('ServerKafka', () => {
|
||||
(server as any).consumer = consumer;
|
||||
(server as any).producer = producer;
|
||||
});
|
||||
it('should close server', () => {
|
||||
server.close();
|
||||
it('should close server', async () => {
|
||||
await server.close();
|
||||
|
||||
expect(consumer.disconnect.calledOnce).to.be.true;
|
||||
expect(producer.disconnect.calledOnce).to.be.true;
|
||||
@@ -229,7 +229,9 @@ describe('ServerKafka', () => {
|
||||
replyPartition,
|
||||
correlationId,
|
||||
);
|
||||
sendMessageStub = sinon.stub(server, 'sendMessage').callsFake(() => ({}));
|
||||
sendMessageStub = sinon
|
||||
.stub(server, 'sendMessage')
|
||||
.callsFake(async () => []);
|
||||
});
|
||||
it(`should return function`, () => {
|
||||
expect(typeof server.getPublisher(null, null, correlationId)).to.be.eql(
|
||||
@@ -258,7 +260,7 @@ describe('ServerKafka', () => {
|
||||
let getPublisherSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(server, 'sendMessage').callsFake(() => ({}));
|
||||
sinon.stub(server, 'sendMessage').callsFake(async () => []);
|
||||
getPublisherSpy = sinon.spy();
|
||||
|
||||
sinon.stub(server, 'getPublisher').callsFake(() => getPublisherSpy);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { RequestMethod } from '@nestjs/common';
|
||||
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';
|
||||
import { isFunction, isNil, isObject } from '@nestjs/common/utils/shared.utils';
|
||||
import { AbstractHttpAdapter } from '@nestjs/core/adapters/http-adapter';
|
||||
@@ -108,7 +111,7 @@ export class ExpressAdapter extends AbstractHttpAdapter {
|
||||
return request.originalUrl;
|
||||
}
|
||||
|
||||
public enableCors(options: CorsOptions) {
|
||||
public enableCors(options: CorsOptions | CorsOptionsDelegate<any>) {
|
||||
return this.use(cors(options));
|
||||
}
|
||||
|
||||
@@ -142,6 +145,11 @@ export class ExpressAdapter extends AbstractHttpAdapter {
|
||||
.forEach(parserKey => this.use(parserMiddleware[parserKey]));
|
||||
}
|
||||
|
||||
public setLocal(key: string, value: any) {
|
||||
this.instance.locals[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getType(): string {
|
||||
return 'express';
|
||||
}
|
||||
|
||||
@@ -75,4 +75,16 @@ export interface NestExpressApplication extends INestApplication {
|
||||
* @returns {this}
|
||||
*/
|
||||
setViewEngine(engine: string): this;
|
||||
|
||||
/**
|
||||
* Sets app-level globals for view templates.
|
||||
*
|
||||
* @example
|
||||
* app.setLocal('title', 'My Site')
|
||||
*
|
||||
* @see https://expressjs.com/en/4x/api.html#app.locals
|
||||
*
|
||||
* @returns {this}
|
||||
*/
|
||||
setLocal(key: string, value: any): this;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function AnyFilesInterceptor(
|
||||
): Promise<Observable<any>> {
|
||||
const ctx = context.switchToHttp();
|
||||
|
||||
await new Promise((resolve, reject) =>
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
this.multer.any()(ctx.getRequest(), ctx.getResponse(), (err: any) => {
|
||||
if (err) {
|
||||
const error = transformException(err);
|
||||
|
||||
@@ -43,7 +43,7 @@ export function FileFieldsInterceptor(
|
||||
): Promise<Observable<any>> {
|
||||
const ctx = context.switchToHttp();
|
||||
|
||||
await new Promise((resolve, reject) =>
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
this.multer.fields(uploadFields)(
|
||||
ctx.getRequest(),
|
||||
ctx.getResponse(),
|
||||
|
||||
@@ -40,7 +40,7 @@ export function FileInterceptor(
|
||||
): Promise<Observable<any>> {
|
||||
const ctx = context.switchToHttp();
|
||||
|
||||
await new Promise((resolve, reject) =>
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
this.multer.single(fieldName)(
|
||||
ctx.getRequest(),
|
||||
ctx.getResponse(),
|
||||
|
||||
@@ -41,7 +41,7 @@ export function FilesInterceptor(
|
||||
): Promise<Observable<any>> {
|
||||
const ctx = context.switchToHttp();
|
||||
|
||||
await new Promise((resolve, reject) =>
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
this.multer.array(fieldName, maxCount)(
|
||||
ctx.getRequest(),
|
||||
ctx.getResponse(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/platform-express",
|
||||
"version": "7.5.5",
|
||||
"version": "7.6.7",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@platform-express)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -21,11 +21,11 @@
|
||||
"cors": "2.8.5",
|
||||
"express": "4.17.1",
|
||||
"multer": "1.4.2",
|
||||
"tslib": "2.0.3"
|
||||
"tslib": "2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "7.5.5",
|
||||
"@nestjs/core": "7.5.5"
|
||||
"@nestjs/common": "7.6.7",
|
||||
"@nestjs/core": "7.6.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.0.0",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { HttpStatus, Logger, RequestMethod } from '@nestjs/common';
|
||||
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import {
|
||||
CorsOptions,
|
||||
CorsOptionsDelegate,
|
||||
} from '@nestjs/common/interfaces/external/cors-options.interface';
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { AbstractHttpAdapter } from '@nestjs/core/adapters/http-adapter';
|
||||
import {
|
||||
@@ -59,12 +62,8 @@ type FastifyHttpsOptions<
|
||||
|
||||
export class FastifyAdapter<
|
||||
TServer extends RawServerBase = RawServerDefault,
|
||||
TRawRequest extends RawRequestDefaultExpression<
|
||||
TServer
|
||||
> = RawRequestDefaultExpression<TServer>,
|
||||
TRawResponse extends RawReplyDefaultExpression<
|
||||
TServer
|
||||
> = RawReplyDefaultExpression<TServer>
|
||||
TRawRequest extends RawRequestDefaultExpression<TServer> = RawRequestDefaultExpression<TServer>,
|
||||
TRawResponse extends RawReplyDefaultExpression<TServer> = RawReplyDefaultExpression<TServer>
|
||||
> extends AbstractHttpAdapter<
|
||||
TServer,
|
||||
FastifyRequest<RequestGenericInterface, TServer, TRawRequest>,
|
||||
@@ -113,9 +112,6 @@ export class FastifyAdapter<
|
||||
callback?: () => void,
|
||||
): void;
|
||||
public listen(port: string | number, ...args: any[]): Promise<string> {
|
||||
if (typeof port === 'string') {
|
||||
port = parseInt(port);
|
||||
}
|
||||
return this.instance.listen(port, ...args);
|
||||
}
|
||||
|
||||
@@ -264,8 +260,18 @@ export class FastifyAdapter<
|
||||
return request.raw ? request.raw.url : request.url;
|
||||
}
|
||||
|
||||
public enableCors(options: CorsOptions) {
|
||||
this.register(require('fastify-cors'), options);
|
||||
public enableCors(
|
||||
options:
|
||||
| CorsOptions
|
||||
| CorsOptionsDelegate<
|
||||
FastifyRequest<RequestGenericInterface, TServer, TRawRequest>
|
||||
>,
|
||||
) {
|
||||
if (typeof options === 'function') {
|
||||
this.register(require('fastify-cors'), () => options);
|
||||
} else {
|
||||
this.register(require('fastify-cors'), options);
|
||||
}
|
||||
}
|
||||
|
||||
public registerParserMiddleware() {
|
||||
|
||||
@@ -55,16 +55,16 @@ export interface NestFastifyApplication extends INestApplication {
|
||||
* @returns A Promise that, when resolved, is a reference to the underlying HttpServer.
|
||||
*/
|
||||
listen(
|
||||
port: number,
|
||||
port: number | string,
|
||||
callback?: (err: Error, address: string) => void,
|
||||
): Promise<any>;
|
||||
listen(
|
||||
port: number,
|
||||
port: number | string,
|
||||
address: string,
|
||||
callback?: (err: Error, address: string) => void,
|
||||
): Promise<any>;
|
||||
listen(
|
||||
port: number,
|
||||
port: number | string,
|
||||
address: string,
|
||||
backlog: number,
|
||||
callback?: (err: Error, address: string) => void,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/platform-fastify",
|
||||
"version": "7.5.5",
|
||||
"version": "7.6.7",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@platform-fastify)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -17,13 +17,13 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"fastify": "3.9.1",
|
||||
"fastify-cors": "5.0.0",
|
||||
"fastify": "3.11.0",
|
||||
"fastify-cors": "5.2.0",
|
||||
"fastify-formbody": "5.0.0",
|
||||
"light-my-request": "4.3.0",
|
||||
"light-my-request": "4.4.1",
|
||||
"middie": "5.2.0",
|
||||
"path-to-regexp": "3.2.0",
|
||||
"tslib": "2.0.3"
|
||||
"tslib": "2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.0.0",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user