Skip to content

Commit da0f245

Browse files
Merge pull request #2899 from RedisInsight/release/2.40.0
Release/2.40.0
2 parents 863100f + 3a22ae8 commit da0f245

File tree

24 files changed

+221
-177
lines changed

24 files changed

+221
-177
lines changed

redisinsight/api/config/default.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export default {
6060
tlsKey: process.env.SERVER_TLS_KEY,
6161
staticContent: !!process.env.SERVER_STATIC_CONTENT || false,
6262
buildType: process.env.BUILD_TYPE || 'ELECTRON',
63-
appVersion: process.env.APP_VERSION || '2.38.0',
63+
appVersion: process.env.APP_VERSION || '2.40.0',
6464
requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 25000,
6565
excludeRoutes: [],
6666
excludeAuthRoutes: [],

redisinsight/api/config/swagger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const SWAGGER_CONFIG: Omit<OpenAPIObject, 'paths'> = {
55
info: {
66
title: 'RedisInsight Backend API',
77
description: 'RedisInsight Backend API',
8-
version: '2.38.0',
8+
version: '2.40.0',
99
},
1010
tags: [],
1111
};

redisinsight/api/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redisinsight-api",
3-
"version": "2.38.0",
3+
"version": "2.40.0",
44
"description": "RedisInsight API",
55
"private": true,
66
"author": {
@@ -60,6 +60,7 @@
6060
"analytics-node": "^4.0.1",
6161
"axios": "^0.25.0",
6262
"body-parser": "^1.19.0",
63+
"busboy": "^1.6.0",
6364
"class-transformer": "^0.2.3",
6465
"class-validator": "^0.14.0",
6566
"connect-timeout": "^1.9.0",

redisinsight/api/src/modules/analytics/analytics.service.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ describe('AnalyticsService', () => {
196196
event: TelemetryEvents.ApplicationStarted,
197197
eventData: {},
198198
nonTracking: false,
199+
traits: {
200+
telemetry: 'will be overwritten',
201+
custom: 'trait',
202+
},
199203
});
200204

201205
expect(mockAnalyticsPage).toHaveBeenCalledWith({
@@ -205,6 +209,7 @@ describe('AnalyticsService', () => {
205209
context: {
206210
traits: {
207211
telemetry: Telemetry.Enabled,
212+
custom: 'trait',
208213
},
209214
},
210215
properties: {

redisinsight/api/src/modules/analytics/analytics.service.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface ITelemetryEvent {
1313
event: string;
1414
eventData: Object;
1515
nonTracking: boolean;
16+
traits?: Object;
1617
}
1718

1819
export interface ITelemetryInitEvent {
@@ -43,7 +44,7 @@ export class AnalyticsService {
4344

4445
private appVersion: string = '2.0.0';
4546

46-
private analytics;
47+
private analytics: Analytics;
4748

4849
constructor(
4950
private settingsService: SettingsService,
@@ -80,7 +81,9 @@ export class AnalyticsService {
8081
// for analytics is granted or not.
8182
// If permissions not granted
8283
// anonymousId will includes "00000000-0000-0000-0000-000000000001" value without any user identifiers.
83-
const { event, eventData, nonTracking } = payload;
84+
const {
85+
event, eventData, nonTracking, traits = {},
86+
} = payload;
8487
const isAnalyticsGranted = await this.checkIsAnalyticsGranted();
8588

8689
if (isAnalyticsGranted || nonTracking) {
@@ -90,8 +93,9 @@ export class AnalyticsService {
9093
event,
9194
context: {
9295
traits: {
96+
...traits,
9397
telemetry: isAnalyticsGranted ? Telemetry.Enabled : Telemetry.Disabled,
94-
}
98+
},
9599
},
96100
properties: {
97101
...eventData,
@@ -118,7 +122,9 @@ export class AnalyticsService {
118122
// user in any way. When `nonTracking` is True, the event is sent regardless of whether the user's permission
119123
// for analytics is granted or not.
120124
// If permissions not granted anonymousId includes "UNSET" value without any user identifiers.
121-
const { event, eventData, nonTracking } = payload;
125+
const {
126+
event, eventData, nonTracking, traits = {},
127+
} = payload;
122128
const isAnalyticsGranted = await this.checkIsAnalyticsGranted();
123129

124130
if (isAnalyticsGranted || nonTracking) {
@@ -128,8 +134,9 @@ export class AnalyticsService {
128134
integrations: { Amplitude: { session_id: this.sessionId } },
129135
context: {
130136
traits: {
137+
...traits,
131138
telemetry: isAnalyticsGranted ? Telemetry.Enabled : Telemetry.Disabled,
132-
}
139+
},
133140
},
134141
properties: {
135142
...eventData,

redisinsight/api/src/modules/analytics/dto/analytics.dto.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,13 @@ export class SendEventDto {
3636
@IsOptional()
3737
@IsBoolean()
3838
nonTracking: boolean = false;
39+
40+
@ApiPropertyOptional({
41+
description: 'User data.',
42+
type: Object,
43+
example: { telemetry: true },
44+
})
45+
@IsOptional()
46+
@ValidateNested()
47+
traits: Object = {};
3948
}

redisinsight/api/src/modules/bulk-actions/bulk-import.controller.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import {
22
Body,
33
ClassSerializerInterceptor,
4-
Controller, HttpCode, Post,
4+
Controller, HttpCode, Post, Req,
55
UseInterceptors, UsePipes, ValidationPipe,
66
} from '@nestjs/common';
7+
import * as Busboy from 'busboy';
8+
import { Readable } from 'stream';
9+
import { Request } from 'express';
710
import {
811
ApiConsumes, ApiTags,
912
} from '@nestjs/swagger';
1013
import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';
11-
import { FormDataRequest } from 'nestjs-form-data';
1214
import { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';
13-
import { UploadImportFileDto } from 'src/modules/bulk-actions/dto/upload-import-file.dto';
1415
import { ClientMetadataParam } from 'src/common/decorators';
1516
import { ClientMetadata } from 'src/common/models';
1617
import { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface';
@@ -26,7 +27,6 @@ export class BulkImportController {
2627
@Post('import')
2728
@ApiConsumes('multipart/form-data')
2829
@HttpCode(200)
29-
@FormDataRequest()
3030
@ApiEndpoint({
3131
description: 'Import data from file',
3232
responses: [
@@ -36,10 +36,21 @@ export class BulkImportController {
3636
],
3737
})
3838
async import(
39-
@Body() dto: UploadImportFileDto,
39+
@Req() req: Request,
4040
@ClientMetadataParam() clientMetadata: ClientMetadata,
4141
): Promise<IBulkActionOverview> {
42-
return this.service.import(clientMetadata, dto);
42+
return new Promise((res, rej) => {
43+
const busboy = Busboy({ headers: req.headers });
44+
45+
busboy.on(
46+
'file',
47+
(_fieldName: string, fileStream: Readable) => {
48+
this.service.import(clientMetadata, fileStream).then(res).catch(rej);
49+
},
50+
);
51+
52+
req.pipe(busboy);
53+
});
4354
}
4455

4556
@Post('import/tutorial-data')

redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts

Lines changed: 38 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
mockIORedisClient,
88
mockIORedisCluster, MockType,
99
} from 'src/__mocks__';
10-
import { MemoryStoredFile } from 'nestjs-form-data';
1110
import { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-summary';
1211
import { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface';
1312
import { BulkActionStatus, BulkActionType } from 'src/modules/bulk-actions/constants';
@@ -17,6 +16,7 @@ import * as fs from 'fs-extra';
1716
import config from 'src/utils/config';
1817
import { join } from 'path';
1918
import { wrapHttpError } from 'src/common/utils';
19+
import { Readable } from 'stream';
2020

2121
const PATH_CONFIG = config.get('dir_path');
2222

@@ -71,13 +71,7 @@ const mockEmptyImportResult: IBulkActionOverview = {
7171
duration: 0,
7272
};
7373

74-
const mockUploadImportFileDto = {
75-
file: {
76-
originalname: 'filename',
77-
size: 1,
78-
buffer: Buffer.from('SET foo bar'),
79-
} as unknown as MemoryStoredFile,
80-
};
74+
const mockReadableStream = Readable.from(Buffer.from('SET foo bar'));
8175

8276
const mockUploadImportFileByPathDto = {
8377
path: '/some/path',
@@ -152,7 +146,7 @@ describe('BulkImportService', () => {
152146

153147
it('should import data', async () => {
154148
spy.mockResolvedValue(mockSummary);
155-
expect(await service.import(mockClientMetadata, mockUploadImportFileDto)).toEqual({
149+
expect(await service.import(mockClientMetadata, mockReadableStream)).toEqual({
156150
...mockImportResult,
157151
duration: jasmine.anything(),
158152
});
@@ -168,21 +162,17 @@ describe('BulkImportService', () => {
168162
succeed: 10_000,
169163
failed: 0,
170164
}));
171-
expect(await service.import(mockClientMetadata, {
172-
file: {
173-
...mockUploadImportFileDto.file,
174-
buffer: generateNCommandsBuffer(100_000),
175-
} as unknown as MemoryStoredFile,
176-
})).toEqual({
177-
...mockImportResult,
178-
summary: {
179-
processed: 100_000,
180-
succeed: 100_000,
181-
failed: 0,
182-
errors: [],
183-
},
184-
duration: jasmine.anything(),
185-
});
165+
expect(await service.import(mockClientMetadata, Readable.from(generateNCommandsBuffer(100_000))))
166+
.toEqual({
167+
...mockImportResult,
168+
summary: {
169+
processed: 100_000,
170+
succeed: 100_000,
171+
failed: 0,
172+
errors: [],
173+
},
174+
duration: jasmine.anything(),
175+
});
186176
});
187177

188178
it('should import data (10K) from file in batches 10K each', async () => {
@@ -191,21 +181,17 @@ describe('BulkImportService', () => {
191181
succeed: 10_000,
192182
failed: 0,
193183
}));
194-
expect(await service.import(mockClientMetadata, {
195-
file: {
196-
...mockUploadImportFileDto.file,
197-
buffer: generateNCommandsBuffer(10_000),
198-
} as unknown as MemoryStoredFile,
199-
})).toEqual({
200-
...mockImportResult,
201-
summary: {
202-
processed: 10_000,
203-
succeed: 10_000,
204-
failed: 0,
205-
errors: [],
206-
},
207-
duration: jasmine.anything(),
208-
});
184+
expect(await service.import(mockClientMetadata, Readable.from(generateNCommandsBuffer(10_000))))
185+
.toEqual({
186+
...mockImportResult,
187+
summary: {
188+
processed: 10_000,
189+
succeed: 10_000,
190+
failed: 0,
191+
errors: [],
192+
},
193+
duration: jasmine.anything(),
194+
});
209195
});
210196

211197
it('should not import any data due to parse error', async () => {
@@ -214,12 +200,10 @@ describe('BulkImportService', () => {
214200
succeed: 0,
215201
failed: 0,
216202
}));
217-
expect(await service.import(mockClientMetadata, {
218-
file: {
219-
...mockUploadImportFileDto.file,
220-
buffer: Buffer.from('{"incorrectdata"}\n{"incorrectdata"}'),
221-
} as unknown as MemoryStoredFile,
222-
})).toEqual({
203+
expect(await service.import(
204+
mockClientMetadata,
205+
Readable.from(Buffer.from('{"incorrectdata"}\n{"incorrectdata"}')),
206+
)).toEqual({
223207
...mockImportResult,
224208
summary: {
225209
processed: 2,
@@ -233,21 +217,19 @@ describe('BulkImportService', () => {
233217
});
234218

235219
it('should ignore blank lines', async () => {
236-
await service.import(mockClientMetadata, {
237-
file: {
238-
...mockUploadImportFileDto.file,
239-
buffer: Buffer.from('\n SET foo bar \n \n SET foo bar \n '),
240-
} as unknown as MemoryStoredFile,
241-
})
242-
expect(spy).toBeCalledWith(mockIORedisClient, [['set', ['foo', 'bar']], ['set', ['foo', 'bar']]])
220+
await service.import(
221+
mockClientMetadata,
222+
Readable.from(Buffer.from('\n SET foo bar \n \n SET foo bar \n ')),
223+
);
224+
expect(spy).toBeCalledWith(mockIORedisClient, [['set', ['foo', 'bar']], ['set', ['foo', 'bar']]]);
243225
expect(mockIORedisClient.disconnect).toHaveBeenCalled();
244226
});
245227

246228
it('should throw an error in case of global error', async () => {
247229
try {
248230
databaseConnectionService.createClient.mockRejectedValueOnce(new NotFoundException());
249231

250-
await service.import(mockClientMetadata, mockUploadImportFileDto);
232+
await service.import(mockClientMetadata, mockReadableStream);
251233

252234
fail();
253235
} catch (e) {
@@ -275,15 +257,15 @@ describe('BulkImportService', () => {
275257

276258
await service.uploadFromTutorial(mockClientMetadata, mockUploadImportFileByPathDto);
277259

278-
expect(mockedFs.readFile).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, mockUploadImportFileByPathDto.path));
260+
expect(mockedFs.createReadStream).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, mockUploadImportFileByPathDto.path));
279261
});
280262

281263
it('should import file by path with static', async () => {
282264
mockedFs.pathExists.mockImplementationOnce(async () => true);
283265

284266
await service.uploadFromTutorial(mockClientMetadata, { path: '/static/guides/_data.file' });
285267

286-
expect(mockedFs.readFile).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, '/guides/_data.file'));
268+
expect(mockedFs.createReadStream).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, '/guides/_data.file'));
287269
});
288270

289271
it('should normalize path before importing and not search for file outside home folder', async () => {
@@ -293,7 +275,7 @@ describe('BulkImportService', () => {
293275
path: '/../../../danger',
294276
});
295277

296-
expect(mockedFs.readFile).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, 'danger'));
278+
expect(mockedFs.createReadStream).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, 'danger'));
297279
});
298280

299281
it('should normalize path before importing and throw an error when search for file outside home folder (relative)', async () => {
@@ -324,19 +306,5 @@ describe('BulkImportService', () => {
324306
expect(e.message).toEqual('Data file was not found');
325307
}
326308
});
327-
328-
it('should throw BadRequest when file size is greater then 100MB', async () => {
329-
mockedFs.pathExists.mockImplementationOnce(async () => true);
330-
mockedFs.stat.mockImplementationOnce(async () => ({ size: 100 * 1024 * 1024 + 1 } as fs.Stats));
331-
332-
try {
333-
await service.uploadFromTutorial(mockClientMetadata, mockUploadImportFileByPathDto);
334-
335-
fail();
336-
} catch (e) {
337-
expect(e).toBeInstanceOf(BadRequestException);
338-
expect(e.message).toEqual('Maximum file size is 100MB');
339-
}
340-
});
341309
});
342310
});

0 commit comments

Comments
 (0)