Skip to content

feat(zendesk api): Add helper functions to get dynamic data from Zendesk API #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions __tests__/services/zendesk-api-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,128 @@ describe("ZendeskService", () => {
).rejects.toThrow(RangeError);
expect(requestMock).toHaveBeenCalledTimes(0);
});

describe("getTags", () => {
it("should call the API and return the tags", async () => {
const tags = [{ name: "tag1" }];
requestMock.mockResolvedValueOnce({ tags });

const result = await service.getTags();

expect(requestMock).toHaveBeenCalledWith(`/api/v2/tags`);
expect(result).toEqual(tags);
});

it("should continue calling the API until next_page disappears", async () => {
const tags = [{ name: "tag1" }];
requestMock
.mockResolvedValueOnce({ tags, next_page: "next_page" })
.mockResolvedValueOnce({ tags: [] });

const result = await service.getTags();

expect(requestMock).toHaveBeenCalledTimes(2);
expect(requestMock).toHaveBeenNthCalledWith(1, `/api/v2/tags`);
expect(requestMock).toHaveBeenNthCalledWith(2, "next_page");
expect(result).toEqual(tags);
});

it("should only call the API one time with fetchAllTags set to false", async () => {
const tags = [{ name: "tag1" }];
requestMock.mockResolvedValueOnce({ tags, next_page: "next_page" });

const result = await service.getTags(false);

expect(requestMock).toHaveBeenCalledTimes(1);
expect(requestMock).toHaveBeenCalledWith(`/api/v2/tags`);
expect(result).toEqual(tags);
});
});

describe("getGroups", () => {
it("should call the API and return the groups", async () => {
const groups = [{ name: "group1" }];
requestMock.mockResolvedValueOnce({ groups });

const result = await service.getGroups();

expect(requestMock).toHaveBeenCalledWith(`/api/v2/groups`);
expect(result).toEqual(groups);
});

it("should continue calling the API until next_page disappears", async () => {
const groups = [{ name: "group1" }];
requestMock
.mockResolvedValueOnce({ groups, next_page: "next_page" })
.mockResolvedValueOnce({ groups: [] });

const result = await service.getGroups();

expect(requestMock).toHaveBeenCalledTimes(2);
expect(requestMock).toHaveBeenNthCalledWith(1, `/api/v2/groups`);
expect(requestMock).toHaveBeenNthCalledWith(2, "next_page");
expect(result).toEqual(groups);
});

it("should only call the API one time with fetchAllGroups set to false", async () => {
const groups = [{ name: "group1" }];
requestMock.mockResolvedValueOnce({ groups, next_page: "next_page" });

const result = await service.getGroups(false);

expect(requestMock).toHaveBeenCalledTimes(1);
expect(requestMock).toHaveBeenCalledWith(`/api/v2/groups`);
expect(result).toEqual(groups);
});
});

describe("getOrganizations", () => {
it("should call the API and return the organizations", async () => {
const organizations = [{ name: "organization1" }];
requestMock.mockResolvedValueOnce({ organizations });

const result = await service.getOrganizations();

expect(requestMock).toHaveBeenCalledWith(`/api/v2/organizations`);
expect(result).toEqual(organizations);
});

it("should continue calling the API until next_page disappears", async () => {
const organizations = [{ name: "organization1" }];
requestMock
.mockResolvedValueOnce({ organizations, next_page: "next_page" })
.mockResolvedValueOnce({ organizations: [] });

const result = await service.getOrganizations();

expect(requestMock).toHaveBeenCalledTimes(2);
expect(requestMock).toHaveBeenNthCalledWith(1, `/api/v2/organizations`);
expect(requestMock).toHaveBeenNthCalledWith(2, "next_page");
expect(result).toEqual(organizations);
});

it("should only call the API one time with fetchAllOrganizations set to false", async () => {
const organizations = [{ name: "organization1" }];
requestMock.mockResolvedValueOnce({ organizations, next_page: "next_page" });

const result = await service.getOrganizations(false);

expect(requestMock).toHaveBeenCalledTimes(1);
expect(requestMock).toHaveBeenCalledWith(`/api/v2/organizations`);
expect(result).toEqual(organizations);
});
});
describe("getLocales", () => {
it("should fetch and return locales", async () => {
const locales = [{ locale: "en-US" }];
requestMock.mockResolvedValueOnce({ locales });

const result = await service.getLocales();

expect(requestMock).toHaveBeenCalledWith(`/api/v2/locales`);
expect(result).toEqual(locales);
});
});
});
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zendesk/zaf-toolbox",
"version": "0.2.9",
"version": "0.2.10",
"description": "A toolbox for ZAF application built with 🩷 by Zendesk Labs",
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "@models/requester";
export * from "@models/whats-app-template";
export * from "@models/zendesk-user";
export * from "@models/custom-objects";
export * from "@models/zendesk-api";
59 changes: 59 additions & 0 deletions src/models/zendesk-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export interface IZendeskResponse {
count: number;
next_page: string | null;
previous_page: string | null;
}

export interface IZendeskTag {
count: number;
name: string;
}

export interface IZendeskGroup {
id: number;
name: string;
created_at: string;
updated_at: string;
is_public: boolean;
}

export interface IZendeskOrganizations {
id: number;
name: string;
created_at: string;
updated_at: string;
domain_names: string[];
details: string;
notes: string;
group_id: number | null;
shared_tickets: boolean;
shared_comments: boolean;
tags: string[];
external_id: string | null;
url: string;
}

export interface IZendeskLocale {
id: number;
name: string;
locale: string;
created_at: string;
updated_at: string;
url: string;
}

export interface ITagsResults extends IZendeskResponse {
tags: IZendeskTag[];
}

export interface IGroupsResults extends IZendeskResponse {
groups: IZendeskGroup[];
}

export interface IOrganizationsResults extends IZendeskResponse {
organizations: IZendeskOrganizations[];
}

export interface ILocalesResults {
locales: IZendeskLocale[];
}
8 changes: 2 additions & 6 deletions src/models/zendesk-user.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IZendeskResponse } from "./zendesk-api";

export interface IZendeskUser<T = IZendeskUserFieldValue> {
id: number;
url: string;
Expand Down Expand Up @@ -62,12 +64,6 @@ export interface IKeyTitleUserField {
title: string;
}

interface IZendeskResponse {
count: number;
next_page: string | null;
previous_page: string | null;
}

export interface ISearchUserResults<T = IZendeskUserFieldValue> extends IZendeskResponse {
users: IZendeskUser<T>[];
}
Expand Down
124 changes: 84 additions & 40 deletions src/services/zendesk-api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ import {
IUserFieldsResults,
IZendeskUserField,
IZendeskUserFieldValue,
HttpMethod
HttpMethod,
ITagsResults,
IGroupsResults,
IOrganizationsResults,
ILocalesResults,
IZendeskTag,
IZendeskLocale,
IZendeskGroup,
IZendeskOrganizations
} from "@models/index";
import { convertContentMessageToHtml } from "@utils/convert-content-message-to-html";
import { getFromClient } from "@utils/get-from-client";
Expand All @@ -24,6 +32,32 @@ export const UPDATE_USER_FIELD_MAX_USERS = 90;
export class ZendeskApiService {
public constructor(public client: Client) {}

/**
* Generic method to fetch all paginated results from a given endpoint.
*
* @param url The initial API endpoint URL.
* @param fetchAll Whether to fetch all pages or just the first.
* @param extractArrayFn Function to extract the array of items from the response.
* @returns A promise resolving to a flattened array of all items.
*/
private async fetchAllPaginatedResults<TResponse, TItem>(
url: string,
fetchAll: boolean,
extractArrayFn: (response: TResponse) => TItem[]
): Promise<TItem[]> {
const results: TResponse[] = [await this.client.request<string, TResponse>(url)];

if (fetchAll) {
while (true) {
const nextPage = (results[results.length - 1] as TResponse & { next_page?: string }).next_page;
if (!nextPage) break;
results.push(await this.client.request<string, TResponse>(nextPage));
}
}

return results.flatMap(extractArrayFn);
}

/**
* Retrieve the requirement id from the requirement file. The identifier is only the name of the requirement.
*
Expand Down Expand Up @@ -95,52 +129,24 @@ export class ZendeskApiService {
*/
public async searchUsers<T = IZendeskUserFieldValue>(
query: string,
fetchAllPages = true
fetchAllUsers = true
): Promise<IZendeskUser<T>[]> {
const results = [
await this.client.request<string, ISearchUserResults<T>>(`/api/v2/users/search?query=${encodeURI(query)}`)
];

if (fetchAllPages) {
while (true) {
const nextPage = results[results.length - 1].next_page;

if (!nextPage) {
break;
}

results.push(await this.client.request<string, ISearchUserResults<T>>(nextPage));
}
}

return results
.flat()
.map(({ users }) => users)
.flat();
return this.fetchAllPaginatedResults<ISearchUserResults<T>, IZendeskUser<T>>(
`/api/v2/users/search?query=${encodeURI(query)}`,
fetchAllUsers,
(response) => response.users
);
}

/**
* Fetch all user fields
*/
public async getUserFields(fetchAllFields = true): Promise<IZendeskUserField[]> {
const results = [await this.client.request<string, IUserFieldsResults>(`/api/v2/user_fields`)];

if (fetchAllFields) {
while (true) {
const nextPage = results[results.length - 1].next_page;

if (!nextPage) {
break;
}

results.push(await this.client.request<string, IUserFieldsResults>(nextPage));
}
}

return results
.flat()
.map(({ user_fields }) => user_fields)
.flat();
return this.fetchAllPaginatedResults<IUserFieldsResults, IZendeskUserField>(
`/api/v2/user_fields`,
fetchAllFields,
(response) => response.user_fields
);
}

/**
Expand Down Expand Up @@ -209,4 +215,42 @@ export class ZendeskApiService {
}
});
}
/**
* Fetch all user instance tags
*/
public async getTags(fetchAllTags = true): Promise<IZendeskTag[]> {
return this.fetchAllPaginatedResults<ITagsResults, IZendeskTag>(
`/api/v2/tags`,
fetchAllTags,
(response) => response.tags
);
}
/**
* Fetch all user instance groups
*/
public async getGroups(fetchAllGroups = true): Promise<IZendeskGroup[]> {
return this.fetchAllPaginatedResults<IGroupsResults, IZendeskGroup>(
`/api/v2/groups`,
fetchAllGroups,
(response) => response.groups
);
}
/**
* Fetch all user instance organizations
*/
public async getOrganizations(fetchAllOrganizations = true): Promise<IZendeskOrganizations[]> {
return this.fetchAllPaginatedResults<IOrganizationsResults, IZendeskOrganizations>(
`/api/v2/organizations`,
fetchAllOrganizations,
(response) => response.organizations
);
}
/**
* Fetch all user instance locales
*/
public async getLocales(): Promise<IZendeskLocale[]> {
const results = await this.client.request<string, ILocalesResults>(`/api/v2/locales`);

return results.locales;
}
}