Skip to content

Commit c1288e3

Browse files
authored
Merge pull request #101 from zendesk/pol-mampey/add-dynamic-data-calls
feat(zendesk api): Add helper functions to get dynamic data from Zendesk API
2 parents 20d842d + b908374 commit c1288e3

5 files changed

Lines changed: 268 additions & 46 deletions

File tree

__tests__/services/zendesk-api-service.spec.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,128 @@ describe("ZendeskService", () => {
309309
).rejects.toThrow(RangeError);
310310
expect(requestMock).toHaveBeenCalledTimes(0);
311311
});
312+
313+
describe("getTags", () => {
314+
it("should call the API and return the tags", async () => {
315+
const tags = [{ name: "tag1" }];
316+
requestMock.mockResolvedValueOnce({ tags });
317+
318+
const result = await service.getTags();
319+
320+
expect(requestMock).toHaveBeenCalledWith(`/api/v2/tags`);
321+
expect(result).toEqual(tags);
322+
});
323+
324+
it("should continue calling the API until next_page disappears", async () => {
325+
const tags = [{ name: "tag1" }];
326+
requestMock
327+
.mockResolvedValueOnce({ tags, next_page: "next_page" })
328+
.mockResolvedValueOnce({ tags: [] });
329+
330+
const result = await service.getTags();
331+
332+
expect(requestMock).toHaveBeenCalledTimes(2);
333+
expect(requestMock).toHaveBeenNthCalledWith(1, `/api/v2/tags`);
334+
expect(requestMock).toHaveBeenNthCalledWith(2, "next_page");
335+
expect(result).toEqual(tags);
336+
});
337+
338+
it("should only call the API one time with fetchAllTags set to false", async () => {
339+
const tags = [{ name: "tag1" }];
340+
requestMock.mockResolvedValueOnce({ tags, next_page: "next_page" });
341+
342+
const result = await service.getTags(false);
343+
344+
expect(requestMock).toHaveBeenCalledTimes(1);
345+
expect(requestMock).toHaveBeenCalledWith(`/api/v2/tags`);
346+
expect(result).toEqual(tags);
347+
});
348+
});
349+
350+
describe("getGroups", () => {
351+
it("should call the API and return the groups", async () => {
352+
const groups = [{ name: "group1" }];
353+
requestMock.mockResolvedValueOnce({ groups });
354+
355+
const result = await service.getGroups();
356+
357+
expect(requestMock).toHaveBeenCalledWith(`/api/v2/groups`);
358+
expect(result).toEqual(groups);
359+
});
360+
361+
it("should continue calling the API until next_page disappears", async () => {
362+
const groups = [{ name: "group1" }];
363+
requestMock
364+
.mockResolvedValueOnce({ groups, next_page: "next_page" })
365+
.mockResolvedValueOnce({ groups: [] });
366+
367+
const result = await service.getGroups();
368+
369+
expect(requestMock).toHaveBeenCalledTimes(2);
370+
expect(requestMock).toHaveBeenNthCalledWith(1, `/api/v2/groups`);
371+
expect(requestMock).toHaveBeenNthCalledWith(2, "next_page");
372+
expect(result).toEqual(groups);
373+
});
374+
375+
it("should only call the API one time with fetchAllGroups set to false", async () => {
376+
const groups = [{ name: "group1" }];
377+
requestMock.mockResolvedValueOnce({ groups, next_page: "next_page" });
378+
379+
const result = await service.getGroups(false);
380+
381+
expect(requestMock).toHaveBeenCalledTimes(1);
382+
expect(requestMock).toHaveBeenCalledWith(`/api/v2/groups`);
383+
expect(result).toEqual(groups);
384+
});
385+
});
386+
387+
describe("getOrganizations", () => {
388+
it("should call the API and return the organizations", async () => {
389+
const organizations = [{ name: "organization1" }];
390+
requestMock.mockResolvedValueOnce({ organizations });
391+
392+
const result = await service.getOrganizations();
393+
394+
expect(requestMock).toHaveBeenCalledWith(`/api/v2/organizations`);
395+
expect(result).toEqual(organizations);
396+
});
397+
398+
it("should continue calling the API until next_page disappears", async () => {
399+
const organizations = [{ name: "organization1" }];
400+
requestMock
401+
.mockResolvedValueOnce({ organizations, next_page: "next_page" })
402+
.mockResolvedValueOnce({ organizations: [] });
403+
404+
const result = await service.getOrganizations();
405+
406+
expect(requestMock).toHaveBeenCalledTimes(2);
407+
expect(requestMock).toHaveBeenNthCalledWith(1, `/api/v2/organizations`);
408+
expect(requestMock).toHaveBeenNthCalledWith(2, "next_page");
409+
expect(result).toEqual(organizations);
410+
});
411+
412+
it("should only call the API one time with fetchAllOrganizations set to false", async () => {
413+
const organizations = [{ name: "organization1" }];
414+
requestMock.mockResolvedValueOnce({ organizations, next_page: "next_page" });
415+
416+
const result = await service.getOrganizations(false);
417+
418+
expect(requestMock).toHaveBeenCalledTimes(1);
419+
expect(requestMock).toHaveBeenCalledWith(`/api/v2/organizations`);
420+
expect(result).toEqual(organizations);
421+
});
422+
});
423+
describe("getLocales", () => {
424+
it("should fetch and return locales", async () => {
425+
const locales = [{ locale: "en-US" }];
426+
requestMock.mockResolvedValueOnce({ locales });
427+
428+
const result = await service.getLocales();
429+
430+
expect(requestMock).toHaveBeenCalledWith(`/api/v2/locales`);
431+
expect(result).toEqual(locales);
432+
});
433+
});
312434
});
313435
});
314436
});

src/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from "@models/requester";
88
export * from "@models/whats-app-template";
99
export * from "@models/zendesk-user";
1010
export * from "@models/custom-objects";
11+
export * from "@models/zendesk-api";

src/models/zendesk-api.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export interface IZendeskResponse {
2+
count: number;
3+
next_page: string | null;
4+
previous_page: string | null;
5+
}
6+
7+
export interface IZendeskTag {
8+
count: number;
9+
name: string;
10+
}
11+
12+
export interface IZendeskGroup {
13+
id: number;
14+
name: string;
15+
created_at: string;
16+
updated_at: string;
17+
is_public: boolean;
18+
}
19+
20+
export interface IZendeskOrganizations {
21+
id: number;
22+
name: string;
23+
created_at: string;
24+
updated_at: string;
25+
domain_names: string[];
26+
details: string;
27+
notes: string;
28+
group_id: number | null;
29+
shared_tickets: boolean;
30+
shared_comments: boolean;
31+
tags: string[];
32+
external_id: string | null;
33+
url: string;
34+
}
35+
36+
export interface IZendeskLocale {
37+
id: number;
38+
name: string;
39+
locale: string;
40+
created_at: string;
41+
updated_at: string;
42+
url: string;
43+
}
44+
45+
export interface ITagsResults extends IZendeskResponse {
46+
tags: IZendeskTag[];
47+
}
48+
49+
export interface IGroupsResults extends IZendeskResponse {
50+
groups: IZendeskGroup[];
51+
}
52+
53+
export interface IOrganizationsResults extends IZendeskResponse {
54+
organizations: IZendeskOrganizations[];
55+
}
56+
57+
export interface ILocalesResults {
58+
locales: IZendeskLocale[];
59+
}

src/models/zendesk-user.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { IZendeskResponse } from "./zendesk-api";
2+
13
export interface IZendeskUser<T = IZendeskUserFieldValue> {
24
id: number;
35
url: string;
@@ -62,12 +64,6 @@ export interface IKeyTitleUserField {
6264
title: string;
6365
}
6466

65-
interface IZendeskResponse {
66-
count: number;
67-
next_page: string | null;
68-
previous_page: string | null;
69-
}
70-
7167
export interface ISearchUserResults<T = IZendeskUserFieldValue> extends IZendeskResponse {
7268
users: IZendeskUser<T>[];
7369
}

src/services/zendesk-api-service.ts

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ import {
1010
IUserFieldsResults,
1111
IZendeskUserField,
1212
IZendeskUserFieldValue,
13-
HttpMethod
13+
HttpMethod,
14+
ITagsResults,
15+
IGroupsResults,
16+
IOrganizationsResults,
17+
ILocalesResults,
18+
IZendeskTag,
19+
IZendeskLocale,
20+
IZendeskGroup,
21+
IZendeskOrganizations
1422
} from "@models/index";
1523
import { convertContentMessageToHtml } from "@utils/convert-content-message-to-html";
1624
import { getFromClient } from "@utils/get-from-client";
@@ -24,6 +32,32 @@ export const UPDATE_USER_FIELD_MAX_USERS = 90;
2432
export class ZendeskApiService {
2533
public constructor(public client: Client) {}
2634

35+
/**
36+
* Generic method to fetch all paginated results from a given endpoint.
37+
*
38+
* @param url The initial API endpoint URL.
39+
* @param fetchAll Whether to fetch all pages or just the first.
40+
* @param extractArrayFn Function to extract the array of items from the response.
41+
* @returns A promise resolving to a flattened array of all items.
42+
*/
43+
private async fetchAllPaginatedResults<TResponse, TItem>(
44+
url: string,
45+
fetchAll: boolean,
46+
extractArrayFn: (response: TResponse) => TItem[]
47+
): Promise<TItem[]> {
48+
const results: TResponse[] = [await this.client.request<string, TResponse>(url)];
49+
50+
if (fetchAll) {
51+
while (true) {
52+
const nextPage = (results[results.length - 1] as TResponse & { next_page?: string }).next_page;
53+
if (!nextPage) break;
54+
results.push(await this.client.request<string, TResponse>(nextPage));
55+
}
56+
}
57+
58+
return results.flatMap(extractArrayFn);
59+
}
60+
2761
/**
2862
* Retrieve the requirement id from the requirement file. The identifier is only the name of the requirement.
2963
*
@@ -95,52 +129,24 @@ export class ZendeskApiService {
95129
*/
96130
public async searchUsers<T = IZendeskUserFieldValue>(
97131
query: string,
98-
fetchAllPages = true
132+
fetchAllUsers = true
99133
): Promise<IZendeskUser<T>[]> {
100-
const results = [
101-
await this.client.request<string, ISearchUserResults<T>>(`/api/v2/users/search?query=${encodeURI(query)}`)
102-
];
103-
104-
if (fetchAllPages) {
105-
while (true) {
106-
const nextPage = results[results.length - 1].next_page;
107-
108-
if (!nextPage) {
109-
break;
110-
}
111-
112-
results.push(await this.client.request<string, ISearchUserResults<T>>(nextPage));
113-
}
114-
}
115-
116-
return results
117-
.flat()
118-
.map(({ users }) => users)
119-
.flat();
134+
return this.fetchAllPaginatedResults<ISearchUserResults<T>, IZendeskUser<T>>(
135+
`/api/v2/users/search?query=${encodeURI(query)}`,
136+
fetchAllUsers,
137+
(response) => response.users
138+
);
120139
}
121140

122141
/**
123142
* Fetch all user fields
124143
*/
125144
public async getUserFields(fetchAllFields = true): Promise<IZendeskUserField[]> {
126-
const results = [await this.client.request<string, IUserFieldsResults>(`/api/v2/user_fields`)];
127-
128-
if (fetchAllFields) {
129-
while (true) {
130-
const nextPage = results[results.length - 1].next_page;
131-
132-
if (!nextPage) {
133-
break;
134-
}
135-
136-
results.push(await this.client.request<string, IUserFieldsResults>(nextPage));
137-
}
138-
}
139-
140-
return results
141-
.flat()
142-
.map(({ user_fields }) => user_fields)
143-
.flat();
145+
return this.fetchAllPaginatedResults<IUserFieldsResults, IZendeskUserField>(
146+
`/api/v2/user_fields`,
147+
fetchAllFields,
148+
(response) => response.user_fields
149+
);
144150
}
145151

146152
/**
@@ -209,4 +215,42 @@ export class ZendeskApiService {
209215
}
210216
});
211217
}
218+
/**
219+
* Fetch all user instance tags
220+
*/
221+
public async getTags(fetchAllTags = true): Promise<IZendeskTag[]> {
222+
return this.fetchAllPaginatedResults<ITagsResults, IZendeskTag>(
223+
`/api/v2/tags`,
224+
fetchAllTags,
225+
(response) => response.tags
226+
);
227+
}
228+
/**
229+
* Fetch all user instance groups
230+
*/
231+
public async getGroups(fetchAllGroups = true): Promise<IZendeskGroup[]> {
232+
return this.fetchAllPaginatedResults<IGroupsResults, IZendeskGroup>(
233+
`/api/v2/groups`,
234+
fetchAllGroups,
235+
(response) => response.groups
236+
);
237+
}
238+
/**
239+
* Fetch all user instance organizations
240+
*/
241+
public async getOrganizations(fetchAllOrganizations = true): Promise<IZendeskOrganizations[]> {
242+
return this.fetchAllPaginatedResults<IOrganizationsResults, IZendeskOrganizations>(
243+
`/api/v2/organizations`,
244+
fetchAllOrganizations,
245+
(response) => response.organizations
246+
);
247+
}
248+
/**
249+
* Fetch all user instance locales
250+
*/
251+
public async getLocales(): Promise<IZendeskLocale[]> {
252+
const results = await this.client.request<string, ILocalesResults>(`/api/v2/locales`);
253+
254+
return results.locales;
255+
}
212256
}

0 commit comments

Comments
 (0)