Skip to content

Commit ca96558

Browse files
committed
feat(api): normalize reversed array ranges before issuing command (RI-8219)
ARGETRANGE and ARSCAN require start <= end. The DTOs now accept either order so the UI doesn't have to enforce it; the service swaps the bounds before issuing the command. assertRangeWithinCap continues to guard the span (still symmetric in effect).
1 parent f65e26d commit ca96558

4 files changed

Lines changed: 67 additions & 11 deletions

File tree

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,30 @@ describe('ArrayService', () => {
222222
).rejects.toThrow(BadRequestException);
223223
});
224224

225+
it('should swap reversed bounds before issuing ARGETRANGE', async () => {
226+
when(mockStandaloneRedisClient.sendCommand)
227+
.calledWith([
228+
BrowserToolArrayCommands.ArGetRange,
229+
mockGetArrayRangeDto.keyName,
230+
'0',
231+
'5',
232+
])
233+
.mockResolvedValue(mockArrayRangeWithGaps);
234+
235+
await service.getRange(mockBrowserClientMetadata, {
236+
...mockGetArrayRangeDto,
237+
start: '5',
238+
end: '0',
239+
});
240+
241+
expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledWith([
242+
BrowserToolArrayCommands.ArGetRange,
243+
mockGetArrayRangeDto.keyName,
244+
'0',
245+
'5',
246+
]);
247+
});
248+
225249
it('should rethrow BadRequest on WrongType', async () => {
226250
const replyError: ReplyError = {
227251
...mockRedisWrongTypeError,
@@ -388,6 +412,30 @@ describe('ArrayService', () => {
388412
).rejects.toThrow(BadRequestException);
389413
});
390414

415+
it('should swap reversed bounds before issuing ARSCAN', async () => {
416+
when(mockStandaloneRedisClient.sendCommand)
417+
.calledWith([
418+
BrowserToolArrayCommands.ArScan,
419+
mockGetArrayScanDto.keyName,
420+
'0',
421+
'5',
422+
])
423+
.mockResolvedValue(flatReply);
424+
425+
await service.scan(mockBrowserClientMetadata, {
426+
...mockGetArrayScanDto,
427+
start: '5',
428+
end: '0',
429+
});
430+
431+
expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledWith([
432+
BrowserToolArrayCommands.ArScan,
433+
mockGetArrayScanDto.keyName,
434+
'0',
435+
'5',
436+
]);
437+
});
438+
391439
it('should rethrow BadRequest on WrongType', async () => {
392440
const replyError: ReplyError = {
393441
...mockRedisWrongTypeError,

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,10 @@ export class ArrayService {
8484
}
8585

8686
// Inputs are validated as canonical decimal strings ≤ 2^64-1, so BigInt()
87-
// is safe. Ranges are reversible (start > end).
87+
// is safe. Callers receive the bounds in canonical ascending order so the
88+
// Redis-facing commands always issue start ≤ end (see normalizeRange).
8889
private assertRangeWithinCap(start: string, end: string): void {
89-
const startBig = BigInt(start);
90-
const endBig = BigInt(end);
91-
const span =
92-
startBig > endBig
93-
? startBig - endBig + BigInt(1)
94-
: endBig - startBig + BigInt(1);
90+
const span = BigInt(end) - BigInt(start) + BigInt(1);
9591

9692
if (span > BigInt(ARRAY_RANGE_MAX_ELEMENTS)) {
9793
throw new BadRequestException(
@@ -100,13 +96,21 @@ export class ArrayService {
10096
}
10197
}
10298

99+
// The DTO accepts start/end in either order so the UI doesn't have to
100+
// enforce ordering; swap before forwarding because ARGETRANGE / ARSCAN
101+
// require start ≤ end.
102+
private normalizeRange(start: string, end: string): [string, string] {
103+
return BigInt(start) > BigInt(end) ? [end, start] : [start, end];
104+
}
105+
103106
public async getRange(
104107
clientMetadata: ClientMetadata,
105108
dto: GetArrayRangeDto,
106109
): Promise<GetArrayRangeResponse> {
107110
try {
108111
this.logger.debug('Getting array range.', clientMetadata);
109-
const { keyName, start, end } = dto;
112+
const { keyName } = dto;
113+
const [start, end] = this.normalizeRange(dto.start, dto.end);
110114

111115
this.assertRangeWithinCap(start, end);
112116

@@ -180,7 +184,8 @@ export class ArrayService {
180184
): Promise<GetArrayScanResponse> {
181185
try {
182186
this.logger.debug('Scanning array range.', clientMetadata);
183-
const { keyName, start, end, limit } = dto;
187+
const { keyName, limit } = dto;
188+
const [start, end] = this.normalizeRange(dto.start, dto.end);
184189

185190
// ARSCAN skips empty slots in the response but still walks the index
186191
// range server-side (O(|end-start|+1)). Apply the same span cap as

redisinsight/api/src/modules/browser/array/dto/get.array-range.dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ export class GetArrayRangeDto extends KeyDto {
1515
@ApiProperty({
1616
description:
1717
'End index of the range (inclusive). Unsigned 64-bit integer as string. ' +
18-
'If start > end, the range is returned reversed.',
18+
'May be passed in either order relative to start; the service normalizes ' +
19+
'the pair to ascending order before issuing the command.',
1920
type: String,
2021
example: '99',
2122
})

redisinsight/api/src/modules/browser/array/dto/get.array-scan.dto.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export class GetArrayScanDto extends KeyDto {
1616

1717
@ApiProperty({
1818
description:
19-
'End index of the range (inclusive). Unsigned 64-bit integer as string.',
19+
'End index of the range (inclusive). Unsigned 64-bit integer as string. ' +
20+
'May be passed in either order relative to start; the service normalizes ' +
21+
'the pair to ascending order before issuing the command.',
2022
type: String,
2123
example: '99',
2224
})

0 commit comments

Comments
 (0)