Merge branch '8.0.0' into fix-set-name-property-of-exception-object-equal-to-class-name

This commit is contained in:
Kamil Mysliwiec
2021-01-27 12:09:34 +01:00
committed by GitHub
170 changed files with 371920 additions and 208779 deletions

View File

@@ -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

View 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();
});
});
});
});

View 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();
});
});
});

View File

@@ -0,0 +1,9 @@
import { Controller, Get } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
getGlobals() {
return '';
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
controllers: [AppController],
})
export class AppModule {}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist"
},
"include": [
"src/**/*",
"e2e/**/*"
],
"exclude": [
"node_modules",
]
}

View File

@@ -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'

View File

@@ -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]);

View 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();
}),
);
});
});

View File

@@ -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);

View File

@@ -0,0 +1,4 @@
export class SumDto {
key: string;
numbers: number[];
}

View File

@@ -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,
});
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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

View 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();
});
});

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
controllers: [AppController],
})
export class AppModule {}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist"
},
"include": [
"src/**/*",
"e2e/**/*"
],
"exclude": [
"node_modules"
]
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -3,5 +3,5 @@
"packages": [
"packages/*"
],
"version": "7.5.5"
"version": "7.6.7"
}

2471
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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,

View File

@@ -2,6 +2,7 @@ export enum HttpStatus {
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
EARLYHINTS = 103,
OK = 200,
CREATED = 201,
ACCEPTED = 202,

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
};
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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.
*/

View File

@@ -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.

View File

@@ -1,3 +1,3 @@
export interface Type<T> extends Function {
export interface Type<T = any> extends Function {
new (...args: any[]): T;
}

View File

@@ -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
}
}
}

View File

@@ -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,

View File

@@ -71,7 +71,7 @@ export class ClassSerializerInterceptor implements NestInterceptor {
: plainOrClass;
}
private getContextOptions(
protected getContextOptions(
context: ExecutionContext,
): ClassTransformOptions | undefined {
return (

View File

@@ -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`);
}
}

View File

@@ -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 () => {

View File

@@ -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', () => {

View File

@@ -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';

View File

@@ -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,
):

View File

@@ -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) {}

View File

@@ -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(

View File

@@ -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 };
}

View File

@@ -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> {

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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);
});
}

View File

@@ -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 {

View File

@@ -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
}
}
}

View File

@@ -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,

View File

@@ -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,
);
});
});
}

View File

@@ -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',

View File

@@ -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,
);
}

View File

@@ -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;
});
});

View File

@@ -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', () => {

View File

@@ -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']);
});
});

View File

@@ -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;

View File

@@ -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',

View File

@@ -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']) {

View File

@@ -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(),
),

View File

@@ -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(),
),

View File

@@ -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(),
),

View File

@@ -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()),
),
);
}

View File

@@ -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,

View File

@@ -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.`,
);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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';

View 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,
};
}
}

View File

@@ -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];
}
}

View File

@@ -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;
};

View File

@@ -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> {

View File

@@ -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
}
}
}

View File

@@ -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(

View File

@@ -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);

View File

@@ -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)) {

View File

@@ -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),
};
});

View File

@@ -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,
}),
),
}),
});
});
});
});

View File

@@ -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(),
}),
),
}),
});
});
});
});

View File

@@ -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();

View File

@@ -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);

View File

@@ -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';
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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",

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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