Skip to content

Commit bfe604c

Browse files
Merge pull request #3891 from RedisInsight/feature/RI-6160-Add-a-list-data-type-with-multiple-elements
RI-6160 Add a list data type with multiple elements
2 parents bb55035 + 3081882 commit bfe604c

File tree

25 files changed

+402
-229
lines changed

25 files changed

+402
-229
lines changed

redisinsight/api/src/modules/browser/__mocks__/list.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const mockListElement2 = Buffer.from('Lorem ipsum dolor sit amet2.');
1515
export const mockListElements = [mockListElement];
1616
export const mockPushElementDto: PushElementToListDto = {
1717
keyName: mockKeyDto.keyName,
18-
element: mockListElement,
18+
elements: mockListElements,
1919
destination: ListElementDestination.Tail,
2020
};
2121
export const mockGetListElementsDto: GetListElementsDto = {

redisinsight/api/src/modules/browser/list/dto/push.element-to-list.dto.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { KeyDto } from 'src/modules/browser/keys/dto';
22
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
3-
import { IsDefined, IsEnum } from 'class-validator';
3+
import { IsArray, IsDefined, IsEnum } from 'class-validator';
44
import { IsRedisString, RedisStringType } from 'src/common/decorators';
55
import { RedisString } from 'src/common/constants';
66

@@ -11,13 +11,15 @@ export enum ListElementDestination {
1111

1212
export class PushElementToListDto extends KeyDto {
1313
@ApiProperty({
14-
description: 'List element',
14+
description: 'List element(s)',
1515
type: String,
16+
isArray: true,
1617
})
1718
@IsDefined()
18-
@IsRedisString()
19-
@RedisStringType()
20-
element: RedisString;
19+
@IsArray()
20+
@IsRedisString({ each: true })
21+
@RedisStringType({ each: true })
22+
elements: RedisString[];
2123

2224
@ApiPropertyOptional({
2325
description:

redisinsight/api/src/modules/browser/list/list.service.spec.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('ListService', () => {
8484
.calledWith([
8585
BrowserToolListCommands.LPush,
8686
mockPushElementDto.keyName,
87-
mockPushElementDto.element,
87+
...mockPushElementDto.elements,
8888
])
8989
.mockResolvedValue(1);
9090

@@ -93,6 +93,22 @@ describe('ListService', () => {
9393
).resolves.not.toThrow();
9494
expect(service.createListWithExpiration).not.toHaveBeenCalled();
9595
});
96+
97+
it('create list with expiration and push at the head', async () => {
98+
when(mockStandaloneRedisClient.sendCommand)
99+
.calledWith([
100+
BrowserToolListCommands.LPush,
101+
mockPushElementDto.keyName,
102+
...mockPushElementDto.elements,
103+
])
104+
.mockResolvedValue(1);
105+
106+
await expect(
107+
service.createList(mockBrowserClientMetadata, mockPushElementDto),
108+
).resolves.not.toThrow();
109+
expect(service.createListWithExpiration).not.toHaveBeenCalled();
110+
});
111+
96112
it('key with this name exist', async () => {
97113
when(mockStandaloneRedisClient.sendCommand)
98114
.calledWith([BrowserToolKeysCommands.Exists, mockPushElementDto.keyName])
@@ -120,25 +136,25 @@ describe('ListService', () => {
120136
});
121137

122138
describe('pushElement', () => {
123-
it('succeed to insert element at the tail of the list data type', async () => {
139+
it('succeed to insert element(s) at the tail of the list data type', async () => {
124140
when(mockStandaloneRedisClient.sendCommand)
125141
.calledWith([
126142
BrowserToolListCommands.RPushX,
127143
mockPushElementDto.keyName,
128-
mockPushElementDto.element,
144+
...mockPushElementDto.elements,
129145
])
130146
.mockResolvedValue(1);
131147

132148
await expect(
133149
service.pushElement(mockBrowserClientMetadata, mockPushElementDto),
134150
).resolves.not.toThrow();
135151
});
136-
it('succeed to insert element at the head of the list data type', async () => {
152+
it('succeed to insert element(s) at the head of the list data type', async () => {
137153
when(mockStandaloneRedisClient.sendCommand)
138154
.calledWith([
139155
BrowserToolListCommands.LPushX,
140156
mockPushElementDto.keyName,
141-
mockPushElementDto.element,
157+
...mockPushElementDto.elements,
142158
])
143159
.mockResolvedValue(12);
144160

@@ -154,7 +170,7 @@ describe('ListService', () => {
154170
.calledWith([
155171
BrowserToolListCommands.RPushX,
156172
mockPushElementDto.keyName,
157-
mockPushElementDto.element,
173+
...mockPushElementDto.elements,
158174
])
159175
.mockResolvedValue(0);
160176

@@ -475,7 +491,7 @@ describe('ListService', () => {
475491
it("shouldn't throw error", async () => {
476492
when(mockStandaloneRedisClient.sendPipeline)
477493
.calledWith([
478-
[BrowserToolListCommands.LPush, dto.keyName, dto.element],
494+
[BrowserToolListCommands.RPush, dto.keyName, ...dto.elements],
479495
[BrowserToolKeysCommands.Expire, dto.keyName, dto.expire],
480496
])
481497
.mockResolvedValue([
@@ -490,11 +506,11 @@ describe('ListService', () => {
490506
it('should throw error', async () => {
491507
const replyError: ReplyError = {
492508
...mockRedisNoPermError,
493-
command: 'LPUSH',
509+
command: 'RPUSH',
494510
};
495511
when(mockStandaloneRedisClient.sendPipeline)
496512
.calledWith([
497-
[BrowserToolListCommands.LPush, dto.keyName, dto.element],
513+
[BrowserToolListCommands.RPush, dto.keyName, ...dto.elements],
498514
[BrowserToolKeysCommands.Expire, dto.keyName, dto.expire],
499515
])
500516
.mockResolvedValue([[replyError, []]]);

redisinsight/api/src/modules/browser/list/list.service.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ export class ListService {
6969
): Promise<PushListElementsResponse> {
7070
try {
7171
this.logger.log('Insert element at the tail/head of the list data type.');
72-
const { keyName, element, destination } = dto;
72+
const { keyName, elements, destination } = dto;
7373
const client: RedisClient = await this.databaseClientFactory.getOrCreateClient(clientMetadata);
7474

7575
const total: RedisClientCommandReply = await client.sendCommand([
7676
BrowserToolListCommands[destination === ListElementDestination.Tail ? 'RPushX' : 'LPushX'],
7777
keyName,
78-
element,
78+
...elements,
7979
]);
8080
if (!total) {
8181
this.logger.error(
@@ -234,17 +234,25 @@ export class ListService {
234234
client: RedisClient,
235235
dto: PushElementToListDto,
236236
): Promise<void> {
237-
const { keyName, element } = dto;
238-
await client.sendCommand([BrowserToolListCommands.LPush, keyName, element]);
237+
const { keyName, elements, destination } = dto;
238+
await client.sendCommand([
239+
BrowserToolListCommands[destination === ListElementDestination.Tail ? 'RPush' : 'LPush'],
240+
keyName,
241+
...elements
242+
]);
239243
}
240244

241245
public async createListWithExpiration(
242246
client: RedisClient,
243247
dto: CreateListWithExpireDto,
244248
): Promise<void> {
245-
const { keyName, element, expire } = dto;
249+
const { keyName, elements, expire, destination } = dto;
246250
const transactionResults = await client.sendPipeline([
247-
[BrowserToolListCommands.LPush, keyName, element],
251+
[
252+
BrowserToolListCommands[destination === ListElementDestination.Tail ? 'RPush' : 'LPush'],
253+
keyName,
254+
...elements
255+
],
248256
[BrowserToolKeysCommands.Expire, keyName, expire],
249257
]);
250258
catchMultiTransactionError(transactionResults);

redisinsight/api/test/api/list/POST-databases-id-list.test.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,22 @@ const endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>
1919
// input data schema
2020
const dataSchema = Joi.object({
2121
keyName: Joi.string().allow('').required(),
22-
element: Joi.string().required(),
22+
elements: Joi.array().items(
23+
Joi.custom((value, helpers) => {
24+
if (typeof value === 'string' || Buffer.isBuffer(value)) {
25+
return value;
26+
}
27+
return helpers.error('any.invalid');
28+
}).messages({
29+
'any.invalid': 'elements must be a string or a Buffer',
30+
})
31+
).required(),
2332
expire: Joi.number().integer().allow(null).min(1).max(2147483647),
2433
}).strict();
2534

2635
const validInputData = {
2736
keyName: constants.TEST_LIST_KEY_1,
28-
element: constants.TEST_LIST_ELEMENT_1,
37+
elements: [constants.TEST_LIST_ELEMENT_1],
2938
expire: constants.TEST_LIST_EXPIRE_1,
3039
};
3140

@@ -53,7 +62,7 @@ const createCheckFn = async (testCase) => {
5362
} else {
5463
if (testCase.statusCode === 201) {
5564
expect(await rte.client.exists(testCase.data.keyName)).to.eql(1);
56-
expect(await rte.client.lrange(testCase.data.keyName, 0, 100)).to.eql([testCase.data.element]);
65+
expect(await rte.client.lrange(testCase.data.keyName, 0, 100)).to.eql(testCase.data.elements);
5766
if (testCase.data.expire) {
5867
expect(await rte.client.ttl(testCase.data.keyName)).to.gte(testCase.data.expire - 5);
5968
} else {
@@ -74,7 +83,7 @@ describe('POST /databases/:databases/list', () => {
7483
name: 'Should create list from buff',
7584
data: {
7685
keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,
77-
element: constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1,
86+
elements: [constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1],
7887
},
7988
statusCode: 201,
8089
after: async () => {
@@ -88,7 +97,7 @@ describe('POST /databases/:databases/list', () => {
8897
name: 'Should create list from ascii',
8998
data: {
9099
keyName: constants.TEST_LIST_KEY_BIN_ASCII_1,
91-
element: constants.TEST_LIST_ELEMENT_BIN_ASCII_1,
100+
elements: [constants.TEST_LIST_ELEMENT_BIN_ASCII_1],
92101
},
93102
statusCode: 201,
94103
after: async () => {
@@ -116,15 +125,15 @@ describe('POST /databases/:databases/list', () => {
116125
name: 'Should create item with empty value',
117126
data: {
118127
keyName: constants.getRandomString(),
119-
element: '',
128+
elements: [''],
120129
},
121130
statusCode: 201,
122131
},
123132
{
124133
name: 'Should create item with key ttl',
125134
data: {
126135
keyName: constants.getRandomString(),
127-
element: constants.getRandomString(),
136+
elements: [constants.getRandomString()],
128137
expire: constants.TEST_STRING_EXPIRE_1,
129138
},
130139
statusCode: 201,
@@ -133,15 +142,15 @@ describe('POST /databases/:databases/list', () => {
133142
name: 'Should create regular item',
134143
data: {
135144
keyName: constants.TEST_LIST_KEY_1,
136-
element: constants.TEST_LIST_ELEMENT_1,
145+
elements: [constants.TEST_LIST_ELEMENT_1],
137146
},
138147
statusCode: 201,
139148
},
140149
{
141150
name: 'Should return conflict error if key already exists',
142151
data: {
143152
keyName: constants.TEST_LIST_KEY_1,
144-
element: constants.getRandomString(),
153+
elements: [constants.getRandomString()],
145154
},
146155
statusCode: 409,
147156
responseBody: {
@@ -158,7 +167,7 @@ describe('POST /databases/:databases/list', () => {
158167
endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),
159168
data: {
160169
keyName: constants.TEST_LIST_KEY_1,
161-
element: constants.getRandomString(),
170+
elements: [constants.getRandomString()],
162171
},
163172
statusCode: 404,
164173
responseBody: {
@@ -183,7 +192,7 @@ describe('POST /databases/:databases/list', () => {
183192
endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),
184193
data: {
185194
keyName: constants.getRandomString(),
186-
element: constants.TEST_LIST_ELEMENT_1,
195+
elements: [constants.TEST_LIST_ELEMENT_1],
187196
},
188197
statusCode: 201,
189198
},
@@ -192,7 +201,7 @@ describe('POST /databases/:databases/list', () => {
192201
endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),
193202
data: {
194203
keyName: constants.getRandomString(),
195-
element: constants.getRandomString(),
204+
elements: [constants.getRandomString()],
196205
},
197206
statusCode: 403,
198207
responseBody: {
@@ -206,7 +215,7 @@ describe('POST /databases/:databases/list', () => {
206215
endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),
207216
data: {
208217
keyName: constants.getRandomString(),
209-
element: constants.getRandomString(),
218+
elements: [constants.getRandomString()],
210219
},
211220
statusCode: 403,
212221
responseBody: {

0 commit comments

Comments
 (0)