Skip to content

Commit d821cd3

Browse files
chore: add params to filter call history by direction and/or state (#37873)
1 parent 38204e0 commit d821cd3

File tree

3 files changed

+157
-31
lines changed

3 files changed

+157
-31
lines changed

apps/meteor/app/api/server/v1/call-history.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
import type { CallHistoryItem, IMediaCall } from '@rocket.chat/core-typings';
1+
import type { CallHistoryItem, CallHistoryItemState, IMediaCall } from '@rocket.chat/core-typings';
22
import { CallHistory, MediaCalls } from '@rocket.chat/models';
33
import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings';
44
import {
55
ajv,
66
validateNotFoundErrorResponse,
77
validateBadRequestErrorResponse,
88
validateUnauthorizedErrorResponse,
9+
validateForbiddenErrorResponse,
910
} from '@rocket.chat/rest-typings';
1011

12+
import { ensureArray } from '../../../../lib/utils/arrayUtils';
1113
import type { ExtractRoutesFromAPI } from '../ApiClass';
1214
import { API } from '../api';
1315
import { getPaginationItems } from '../helpers/getPaginationItems';
1416

15-
type CallHistoryList = PaginatedRequest<Record<never, never>>;
17+
type CallHistoryList = PaginatedRequest<{
18+
direction?: CallHistoryItem['direction'];
19+
state?: CallHistoryItemState[] | CallHistoryItemState;
20+
}>;
1621

1722
const CallHistoryListSchema = {
1823
type: 'object',
@@ -26,6 +31,26 @@ const CallHistoryListSchema = {
2631
sort: {
2732
type: 'string',
2833
},
34+
direction: {
35+
type: 'string',
36+
enum: ['inbound', 'outbound'],
37+
},
38+
state: {
39+
// our clients serialize arrays as `state=value1&state=value2`, but if there's a single value the parser doesn't know it is an array, so we need to support both arrays and direct values
40+
// if a client tries to send a JSON array, our parser will treat it as a string and the type validation will reject it
41+
// This means this param won't work from Swagger UI
42+
oneOf: [
43+
{
44+
type: 'array',
45+
items: {
46+
$ref: '#/components/schemas/CallHistoryItemState',
47+
},
48+
},
49+
{
50+
$ref: '#/components/schemas/CallHistoryItemState',
51+
},
52+
],
53+
},
2954
},
3055
required: [],
3156
additionalProperties: false,
@@ -71,7 +96,8 @@ const callHistoryListEndpoints = API.v1.get(
7196
required: ['count', 'offset', 'total', 'items', 'success'],
7297
}),
7398
400: validateBadRequestErrorResponse,
74-
403: validateUnauthorizedErrorResponse,
99+
401: validateUnauthorizedErrorResponse,
100+
403: validateForbiddenErrorResponse,
75101
},
76102
query: isCallHistoryListProps,
77103
authRequired: true,
@@ -80,11 +106,17 @@ const callHistoryListEndpoints = API.v1.get(
80106
const { offset, count } = await getPaginationItems(this.queryParams as Record<string, string | number | null | undefined>);
81107
const { sort } = await this.parseJsonQuery();
82108

83-
const filter = {
109+
const { direction, state } = this.queryParams;
110+
111+
const stateFilter = state && ensureArray(state);
112+
113+
const query = {
84114
uid: this.userId,
115+
...(direction && { direction }),
116+
...(stateFilter?.length && { state: { $in: stateFilter } }),
85117
};
86118

87-
const { cursor, totalCount } = CallHistory.findPaginated(filter, {
119+
const { cursor, totalCount } = CallHistory.findPaginated(query, {
88120
sort: sort || { ts: -1 },
89121
skip: offset,
90122
limit: count,
@@ -162,7 +194,8 @@ const callHistoryInfoEndpoints = API.v1.get(
162194
required: ['item', 'success'],
163195
}),
164196
400: validateBadRequestErrorResponse,
165-
403: validateUnauthorizedErrorResponse,
197+
401: validateUnauthorizedErrorResponse,
198+
403: validateForbiddenErrorResponse,
166199
404: validateNotFoundErrorResponse,
167200
},
168201
query: isCallHistoryInfoProps,

apps/meteor/server/startup/callHistoryTestData.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import { CallHistory, MediaCalls } from '@rocket.chat/models';
33
export async function addCallHistoryTestData(uid: string, extraUid: string): Promise<void> {
44
const callId1 = 'rocketchat.internal.call.test';
55
const callId2 = 'rocketchat.internal.call.test.2';
6-
const callId3 = 'rocketchat.internal.call.test.3';
7-
const callId4 = 'rocketchat.internal.call.test.4';
6+
const callId3 = 'rocketchat.external.call.test.outbound';
7+
const callId4 = 'rocketchat.external.call.test.inbound';
88

99
await CallHistory.deleteMany({ uid });
1010
await MediaCalls.deleteMany({ _id: { $in: [callId1, callId2, callId3, callId4] } });
1111

1212
await CallHistory.insertMany([
1313
{
14-
_id: 'rocketchat.internal.history.test',
14+
_id: 'rocketchat.internal.history.test.outbound',
1515
ts: new Date(),
1616
callId: callId1,
1717
state: 'ended',
@@ -24,10 +24,10 @@ export async function addCallHistoryTestData(uid: string, extraUid: string): Pro
2424
direction: 'outbound',
2525
},
2626
{
27-
_id: 'rocketchat.internal.history.test.2',
27+
_id: 'rocketchat.internal.history.test.inbound',
2828
ts: new Date(),
2929
callId: callId2,
30-
state: 'ended',
30+
state: 'not-answered',
3131
type: 'media-call',
3232
duration: 10,
3333
endedAt: new Date(),
@@ -37,10 +37,10 @@ export async function addCallHistoryTestData(uid: string, extraUid: string): Pro
3737
direction: 'inbound',
3838
},
3939
{
40-
_id: 'rocketchat.internal.history.test.3',
40+
_id: 'rocketchat.external.history.test.outbound',
4141
ts: new Date(),
4242
callId: callId3,
43-
state: 'ended',
43+
state: 'failed',
4444
type: 'media-call',
4545
duration: 10,
4646
endedAt: new Date(),
@@ -50,7 +50,7 @@ export async function addCallHistoryTestData(uid: string, extraUid: string): Pro
5050
contactExtension: '1001',
5151
},
5252
{
53-
_id: 'rocketchat.internal.history.test.4',
53+
_id: 'rocketchat.external.history.test.inbound',
5454
ts: new Date(),
5555
callId: callId4,
5656
state: 'ended',

apps/meteor/tests/end-to-end/api/call-history.ts

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ describe('[Call History]', () => {
3737
expect(res.body).to.have.property('count', 4);
3838

3939
const historyIds = res.body.items.map((item: any) => item._id);
40-
expect(historyIds).to.include('rocketchat.internal.history.test');
41-
expect(historyIds).to.include('rocketchat.internal.history.test.2');
42-
expect(historyIds).to.include('rocketchat.internal.history.test.3');
43-
expect(historyIds).to.include('rocketchat.internal.history.test.4');
40+
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
41+
expect(historyIds).to.include('rocketchat.internal.history.test.inbound');
42+
expect(historyIds).to.include('rocketchat.external.history.test.outbound');
43+
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
4444

45-
const internalItem1 = res.body.items.find((item: any) => item._id === 'rocketchat.internal.history.test');
45+
const internalItem1 = res.body.items.find((item: any) => item._id === 'rocketchat.internal.history.test.outbound');
4646
expect(internalItem1).to.have.property('callId', 'rocketchat.internal.call.test');
4747
expect(internalItem1).to.have.property('state', 'ended');
4848
expect(internalItem1).to.have.property('type', 'media-call');
@@ -51,26 +51,26 @@ describe('[Call History]', () => {
5151
expect(internalItem1).to.have.property('direction', 'outbound');
5252
expect(internalItem1).to.have.property('contactId');
5353

54-
const internalItem2 = res.body.items.find((item: any) => item._id === 'rocketchat.internal.history.test.2');
54+
const internalItem2 = res.body.items.find((item: any) => item._id === 'rocketchat.internal.history.test.inbound');
5555
expect(internalItem2).to.have.property('callId', 'rocketchat.internal.call.test.2');
56-
expect(internalItem2).to.have.property('state', 'ended');
56+
expect(internalItem2).to.have.property('state', 'not-answered');
5757
expect(internalItem2).to.have.property('type', 'media-call');
5858
expect(internalItem2).to.have.property('duration', 10);
5959
expect(internalItem2).to.have.property('external', false);
6060
expect(internalItem2).to.have.property('direction', 'inbound');
6161
expect(internalItem2).to.have.property('contactId');
6262

63-
const externalItem1 = res.body.items.find((item: any) => item._id === 'rocketchat.internal.history.test.3');
64-
expect(externalItem1).to.have.property('callId', 'rocketchat.internal.call.test.3');
65-
expect(externalItem1).to.have.property('state', 'ended');
63+
const externalItem1 = res.body.items.find((item: any) => item._id === 'rocketchat.external.history.test.outbound');
64+
expect(externalItem1).to.have.property('callId', 'rocketchat.external.call.test.outbound');
65+
expect(externalItem1).to.have.property('state', 'failed');
6666
expect(externalItem1).to.have.property('type', 'media-call');
6767
expect(externalItem1).to.have.property('duration', 10);
6868
expect(externalItem1).to.have.property('external', true);
6969
expect(externalItem1).to.have.property('direction', 'outbound');
7070
expect(externalItem1).to.have.property('contactExtension', '1001');
7171

72-
const externalItem2 = res.body.items.find((item: any) => item._id === 'rocketchat.internal.history.test.4');
73-
expect(externalItem2).to.have.property('callId', 'rocketchat.internal.call.test.4');
72+
const externalItem2 = res.body.items.find((item: any) => item._id === 'rocketchat.external.history.test.inbound');
73+
expect(externalItem2).to.have.property('callId', 'rocketchat.external.call.test.inbound');
7474
expect(externalItem2).to.have.property('state', 'ended');
7575
expect(externalItem2).to.have.property('type', 'media-call');
7676
expect(externalItem2).to.have.property('duration', 10);
@@ -95,6 +95,99 @@ describe('[Call History]', () => {
9595
expect(res.body).to.have.property('count', 0);
9696
});
9797
});
98+
99+
it('should apply filter by state', async () => {
100+
await request
101+
.get(api('call-history.list'))
102+
.set(credentials)
103+
.query({
104+
state: ['ended'],
105+
})
106+
.expect('Content-Type', 'application/json')
107+
.expect(200)
108+
.expect((res: Response) => {
109+
expect(res.body).to.have.property('success', true);
110+
expect(res.body).to.have.property('items').that.is.an('array');
111+
112+
expect(res.body.items).to.have.lengthOf(2);
113+
expect(res.body).to.have.property('total', 2);
114+
expect(res.body).to.have.property('count', 2);
115+
116+
const historyIds = res.body.items.map((item: any) => item._id);
117+
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
118+
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
119+
});
120+
});
121+
122+
it('should apply filter by multiple states', async () => {
123+
await request
124+
.get(api('call-history.list'))
125+
.set(credentials)
126+
.query({
127+
state: ['failed', 'ended'],
128+
})
129+
.expect('Content-Type', 'application/json')
130+
.expect(200)
131+
.expect((res: Response) => {
132+
expect(res.body).to.have.property('success', true);
133+
expect(res.body).to.have.property('items').that.is.an('array');
134+
135+
expect(res.body.items).to.have.lengthOf(3);
136+
expect(res.body).to.have.property('total', 3);
137+
expect(res.body).to.have.property('count', 3);
138+
139+
const historyIds = res.body.items.map((item: any) => item._id);
140+
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
141+
expect(historyIds).to.include('rocketchat.external.history.test.outbound');
142+
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
143+
});
144+
});
145+
146+
it('should apply filter by direction', async () => {
147+
await request
148+
.get(api('call-history.list'))
149+
.set(credentials)
150+
.query({
151+
direction: 'inbound',
152+
})
153+
.expect('Content-Type', 'application/json')
154+
.expect(200)
155+
.expect((res: Response) => {
156+
expect(res.body).to.have.property('success', true);
157+
expect(res.body).to.have.property('items').that.is.an('array');
158+
159+
expect(res.body.items).to.have.lengthOf(2);
160+
expect(res.body).to.have.property('total', 2);
161+
expect(res.body).to.have.property('count', 2);
162+
163+
const historyIds = res.body.items.map((item: any) => item._id);
164+
expect(historyIds).to.include('rocketchat.internal.history.test.inbound');
165+
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
166+
});
167+
});
168+
169+
it('should apply filter by state and direction', async () => {
170+
await request
171+
.get(api('call-history.list'))
172+
.set(credentials)
173+
.query({
174+
state: ['failed', 'ended'],
175+
direction: 'inbound',
176+
})
177+
.expect('Content-Type', 'application/json')
178+
.expect(200)
179+
.expect((res: Response) => {
180+
expect(res.body).to.have.property('success', true);
181+
expect(res.body).to.have.property('items').that.is.an('array');
182+
183+
expect(res.body.items).to.have.lengthOf(1);
184+
expect(res.body).to.have.property('total', 1);
185+
expect(res.body).to.have.property('count', 1);
186+
187+
const historyIds = res.body.items.map((item: any) => item._id);
188+
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
189+
});
190+
});
98191
});
99192

100193
describe('[/call-history.info]', () => {
@@ -103,7 +196,7 @@ describe('[Call History]', () => {
103196
.get(api('call-history.info'))
104197
.set(credentials)
105198
.query({
106-
historyId: 'rocketchat.internal.history.test',
199+
historyId: 'rocketchat.internal.history.test.outbound',
107200
})
108201
.expect('Content-Type', 'application/json')
109202
.expect(200)
@@ -113,7 +206,7 @@ describe('[Call History]', () => {
113206
expect(res.body).to.have.property('call').that.is.an('object');
114207

115208
const { item, call } = res.body;
116-
expect(item).to.have.property('_id', 'rocketchat.internal.history.test');
209+
expect(item).to.have.property('_id', 'rocketchat.internal.history.test.outbound');
117210
expect(item).to.have.property('callId', 'rocketchat.internal.call.test');
118211
expect(item).to.have.property('state', 'ended');
119212
expect(item).to.have.property('type', 'media-call');
@@ -158,9 +251,9 @@ describe('[Call History]', () => {
158251
expect(res.body).to.have.property('call').that.is.an('object');
159252

160253
const { item, call } = res.body;
161-
expect(item).to.have.property('_id', 'rocketchat.internal.history.test.2');
254+
expect(item).to.have.property('_id', 'rocketchat.internal.history.test.inbound');
162255
expect(item).to.have.property('callId', 'rocketchat.internal.call.test.2');
163-
expect(item).to.have.property('state', 'ended');
256+
expect(item).to.have.property('state', 'not-answered');
164257
expect(item).to.have.property('type', 'media-call');
165258
expect(item).to.have.property('duration', 10);
166259
expect(item).to.have.property('external', false);
@@ -215,7 +308,7 @@ describe('[Call History]', () => {
215308
.get(api('call-history.info'))
216309
.set(userCredentials)
217310
.query({
218-
historyId: 'rocketchat.internal.history.test',
311+
historyId: 'rocketchat.internal.history.test.outbound',
219312
})
220313
.expect('Content-Type', 'application/json')
221314
.expect(404);

0 commit comments

Comments
 (0)