From 85353f5f57c68a774c7f269549f0b7cedf1fdfb3 Mon Sep 17 00:00:00 2001 From: Pol Mampey Date: Tue, 22 Apr 2025 15:15:41 -0400 Subject: [PATCH 1/5] feat(zendesk api): Add helper functions to get tags, groups, organizations and locales from Zendesk API --- .../services/zendesk-api-service.spec.ts | 148 ++++++++++++++++++ src/models/zendesk-user.ts | 70 +++++++++ src/services/zendesk-api-service.ts | 100 ++++++++++-- 3 files changed, 309 insertions(+), 9 deletions(-) diff --git a/__tests__/services/zendesk-api-service.spec.ts b/__tests__/services/zendesk-api-service.spec.ts index 96b5abe..67f375f 100644 --- a/__tests__/services/zendesk-api-service.spec.ts +++ b/__tests__/services/zendesk-api-service.spec.ts @@ -309,6 +309,154 @@ 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 call the API and return the 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); + }); + + it("should continue calling the API until next_page disappears", async () => { + const locales = [{ locale: "en-US" }]; + requestMock + .mockResolvedValueOnce({ locales, next_page: "next_page" }) + .mockResolvedValueOnce({ locales: [] }); + + const result = await service.getLocales(); + + expect(requestMock).toHaveBeenCalledTimes(2); + expect(requestMock).toHaveBeenNthCalledWith(1, `/api/v2/locales`); + expect(requestMock).toHaveBeenNthCalledWith(2, "next_page"); + expect(result).toEqual(locales); + }); + + it("should only call the API one time with fetchAllLocales set to false", async () => { + const locales = [{ locale: "en-US" }]; + requestMock.mockResolvedValueOnce({ locales, next_page: "next_page" }); + + const result = await service.getLocales(false); + + expect(requestMock).toHaveBeenCalledTimes(1); + expect(requestMock).toHaveBeenCalledWith(`/api/v2/locales`); + expect(result).toEqual(locales); + }); + }); }); }); }); diff --git a/src/models/zendesk-user.ts b/src/models/zendesk-user.ts index 4bd0125..24ac7ca 100644 --- a/src/models/zendesk-user.ts +++ b/src/models/zendesk-user.ts @@ -62,6 +62,60 @@ export interface IKeyTitleUserField { title: string; } +export interface IZendeskTagsResults extends IZendeskResponse { + tags: IZendeskTag[]; +} + +export interface IZendeskTag { + count: number; + name: string; +} + +export interface IZendeskGroupsResults extends IZendeskResponse { + groups: IZendeskGroup[]; +} + +export interface IZendeskGroup { + id: number; + name: string; + created_at: string; + updated_at: string; + is_public: boolean; +} + +export interface IZendeskOrganizationsResults extends IZendeskResponse { + organizations: IZendeskOrganizations[]; +} + +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 IZendeskLocalesResults extends IZendeskResponse { + locales: IZendeskLocale[]; +} + +export interface IZendeskLocale { + id: number; + name: string; + locale: string; + created_at: string; + updated_at: string; + url: string; +} + interface IZendeskResponse { count: number; next_page: string | null; @@ -75,3 +129,19 @@ export interface ISearchUserResults extends IZendesk export interface IUserFieldsResults extends IZendeskResponse { user_fields: IZendeskUserField[]; } + +export interface ITagsResults extends IZendeskResponse { + tags: IZendeskTagsResults; +} + +export interface IGroupsResults extends IZendeskResponse { + groups: IZendeskGroupsResults; +} + +export interface IOrganizationsResults extends IZendeskResponse { + organizations: IZendeskOrganizationsResults; +} + +export interface ILocalesResults extends IZendeskResponse { + locales: IZendeskLocalesResults; +} diff --git a/src/services/zendesk-api-service.ts b/src/services/zendesk-api-service.ts index ce9ec66..2eb8ccc 100644 --- a/src/services/zendesk-api-service.ts +++ b/src/services/zendesk-api-service.ts @@ -10,7 +10,15 @@ import { IUserFieldsResults, IZendeskUserField, IZendeskUserFieldValue, - HttpMethod + HttpMethod, + ITagsResults, + IZendeskTagsResults, + IGroupsResults, + IZendeskGroupsResults, + IZendeskOrganizationsResults, + IOrganizationsResults, + IZendeskLocalesResults, + ILocalesResults } from "@models/index"; import { convertContentMessageToHtml } from "@utils/convert-content-message-to-html"; import { getFromClient } from "@utils/get-from-client"; @@ -113,10 +121,7 @@ export class ZendeskApiService { } } - return results - .flat() - .map(({ users }) => users) - .flat(); + return results.map(({ users }) => users).flat(); } /** @@ -137,10 +142,7 @@ export class ZendeskApiService { } } - return results - .flat() - .map(({ user_fields }) => user_fields) - .flat(); + return results.map(({ user_fields }) => user_fields).flat(); } /** @@ -209,4 +211,84 @@ export class ZendeskApiService { } }); } + /** + * Fetch all user instance tags + */ + public async getTags(fetchAllTags = true): Promise { + const results = [await this.client.request(`/api/v2/tags`)]; + + if (fetchAllTags) { + while (true) { + const nextPage = results[results.length - 1].next_page; + + if (!nextPage) { + break; + } + + results.push(await this.client.request(nextPage)); + } + } + + return results.map(({ tags }) => tags).flat(); + } + /** + * Fetch all user instance groups + */ + public async getGroups(fetchAllGroups = true): Promise { + const results = [await this.client.request(`/api/v2/groups`)]; + + if (fetchAllGroups) { + while (true) { + const nextPage = results[results.length - 1].next_page; + + if (!nextPage) { + break; + } + + results.push(await this.client.request(nextPage)); + } + } + + return results.map(({ groups }) => groups).flat(); + } + /** + * Fetch all user instance organizations + */ + public async getOrganizations(fetchAllOrganizations = true): Promise { + const results = [await this.client.request(`/api/v2/organizations`)]; + + if (fetchAllOrganizations) { + while (true) { + const nextPage = results[results.length - 1].next_page; + + if (!nextPage) { + break; + } + + results.push(await this.client.request(nextPage)); + } + } + + return results.map(({ organizations }) => organizations).flat(); + } + /** + * Fetch all user instance locales + */ + public async getLocales(fetchAllLocales = true): Promise { + const results = [await this.client.request(`/api/v2/locales`)]; + + if (fetchAllLocales) { + while (true) { + const nextPage = results[results.length - 1].next_page; + + if (!nextPage) { + break; + } + + results.push(await this.client.request(nextPage)); + } + } + + return results.map(({ locales }) => locales).flat(); + } } From 2c58793ebb32286cfb72b35697784a6ad6ce6572 Mon Sep 17 00:00:00 2001 From: pol-mampey Date: Tue, 22 Apr 2025 19:17:40 +0000 Subject: [PATCH 2/5] [BOT] Bump version from 0.2.9 to 0.2.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a349f2..20d6cba 100644 --- a/package.json +++ b/package.json @@ -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", From 66edaab2daeaef5b3128ea5829070b90ed6f2d38 Mon Sep 17 00:00:00 2001 From: pol-mampey Date: Tue, 22 Apr 2025 19:17:42 +0000 Subject: [PATCH 3/5] [BOT] Bump version from 0.2.9 to 0.2.10 From f4888cffab38e18005299f642a9b2ba58d0db679 Mon Sep 17 00:00:00 2001 From: Pol Mampey Date: Thu, 24 Apr 2025 11:16:59 -0400 Subject: [PATCH 4/5] feat(zendesk api): Change Zendesk API helper functions to use common call function to avoid having the same logic multiple times in the same file --- src/models/index.ts | 1 + src/models/zendesk-api.ts | 59 ++++++++++ src/models/zendesk-user.ts | 78 +------------ src/services/zendesk-api-service.ts | 164 +++++++++++----------------- 4 files changed, 125 insertions(+), 177 deletions(-) create mode 100644 src/models/zendesk-api.ts diff --git a/src/models/index.ts b/src/models/index.ts index 0a4d9bb..9c33efc 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -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"; diff --git a/src/models/zendesk-api.ts b/src/models/zendesk-api.ts new file mode 100644 index 0000000..7121852 --- /dev/null +++ b/src/models/zendesk-api.ts @@ -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[]; +} diff --git a/src/models/zendesk-user.ts b/src/models/zendesk-user.ts index 24ac7ca..2fe18df 100644 --- a/src/models/zendesk-user.ts +++ b/src/models/zendesk-user.ts @@ -1,3 +1,5 @@ +import { IZendeskResponse } from "./zendesk-api"; + export interface IZendeskUser { id: number; url: string; @@ -62,66 +64,6 @@ export interface IKeyTitleUserField { title: string; } -export interface IZendeskTagsResults extends IZendeskResponse { - tags: IZendeskTag[]; -} - -export interface IZendeskTag { - count: number; - name: string; -} - -export interface IZendeskGroupsResults extends IZendeskResponse { - groups: IZendeskGroup[]; -} - -export interface IZendeskGroup { - id: number; - name: string; - created_at: string; - updated_at: string; - is_public: boolean; -} - -export interface IZendeskOrganizationsResults extends IZendeskResponse { - organizations: IZendeskOrganizations[]; -} - -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 IZendeskLocalesResults extends IZendeskResponse { - locales: IZendeskLocale[]; -} - -export interface IZendeskLocale { - id: number; - name: string; - locale: string; - created_at: string; - updated_at: string; - url: string; -} - -interface IZendeskResponse { - count: number; - next_page: string | null; - previous_page: string | null; -} - export interface ISearchUserResults extends IZendeskResponse { users: IZendeskUser[]; } @@ -129,19 +71,3 @@ export interface ISearchUserResults extends IZendesk export interface IUserFieldsResults extends IZendeskResponse { user_fields: IZendeskUserField[]; } - -export interface ITagsResults extends IZendeskResponse { - tags: IZendeskTagsResults; -} - -export interface IGroupsResults extends IZendeskResponse { - groups: IZendeskGroupsResults; -} - -export interface IOrganizationsResults extends IZendeskResponse { - organizations: IZendeskOrganizationsResults; -} - -export interface ILocalesResults extends IZendeskResponse { - locales: IZendeskLocalesResults; -} diff --git a/src/services/zendesk-api-service.ts b/src/services/zendesk-api-service.ts index 2eb8ccc..539e95c 100644 --- a/src/services/zendesk-api-service.ts +++ b/src/services/zendesk-api-service.ts @@ -12,13 +12,13 @@ import { IZendeskUserFieldValue, HttpMethod, ITagsResults, - IZendeskTagsResults, IGroupsResults, - IZendeskGroupsResults, - IZendeskOrganizationsResults, IOrganizationsResults, - IZendeskLocalesResults, - ILocalesResults + ILocalesResults, + IZendeskTag, + IZendeskLocale, + IZendeskGroup, + IZendeskOrganizations } from "@models/index"; import { convertContentMessageToHtml } from "@utils/convert-content-message-to-html"; import { getFromClient } from "@utils/get-from-client"; @@ -32,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( + url: string, + fetchAll: boolean, + extractArrayFn: (response: TResponse) => TItem[] + ): Promise { + const results: TResponse[] = [await this.client.request(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(nextPage)); + } + } + + return results.flatMap(extractArrayFn); + } + /** * Retrieve the requirement id from the requirement file. The identifier is only the name of the requirement. * @@ -103,46 +129,24 @@ export class ZendeskApiService { */ public async searchUsers( query: string, - fetchAllPages = true + fetchAllUsers = true ): Promise[]> { - const results = [ - await this.client.request>(`/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>(nextPage)); - } - } - - return results.map(({ users }) => users).flat(); + return this.fetchAllPaginatedResults, IZendeskUser>( + `/api/v2/users/search?query=${encodeURI(query)}`, + fetchAllUsers, + (response) => response.users + ); } /** * Fetch all user fields */ public async getUserFields(fetchAllFields = true): Promise { - const results = [await this.client.request(`/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(nextPage)); - } - } - - return results.map(({ user_fields }) => user_fields).flat(); + return this.fetchAllPaginatedResults( + `/api/v2/tags`, + fetchAllFields, + (response) => response.user_fields + ); } /** @@ -214,81 +218,39 @@ export class ZendeskApiService { /** * Fetch all user instance tags */ - public async getTags(fetchAllTags = true): Promise { - const results = [await this.client.request(`/api/v2/tags`)]; - - if (fetchAllTags) { - while (true) { - const nextPage = results[results.length - 1].next_page; - - if (!nextPage) { - break; - } - - results.push(await this.client.request(nextPage)); - } - } - - return results.map(({ tags }) => tags).flat(); + public async getTags(fetchAllTags = true): Promise { + return this.fetchAllPaginatedResults( + `/api/v2/tags`, + fetchAllTags, + (response) => response.tags + ); } /** * Fetch all user instance groups */ - public async getGroups(fetchAllGroups = true): Promise { - const results = [await this.client.request(`/api/v2/groups`)]; - - if (fetchAllGroups) { - while (true) { - const nextPage = results[results.length - 1].next_page; - - if (!nextPage) { - break; - } - - results.push(await this.client.request(nextPage)); - } - } - - return results.map(({ groups }) => groups).flat(); + public async getGroups(fetchAllGroups = true): Promise { + return this.fetchAllPaginatedResults( + `/api/v2/groups`, + fetchAllGroups, + (response) => response.groups + ); } /** * Fetch all user instance organizations */ - public async getOrganizations(fetchAllOrganizations = true): Promise { - const results = [await this.client.request(`/api/v2/organizations`)]; - - if (fetchAllOrganizations) { - while (true) { - const nextPage = results[results.length - 1].next_page; - - if (!nextPage) { - break; - } - - results.push(await this.client.request(nextPage)); - } - } - - return results.map(({ organizations }) => organizations).flat(); + public async getOrganizations(fetchAllOrganizations = true): Promise { + return this.fetchAllPaginatedResults( + `/api/v2/organizations`, + fetchAllOrganizations, + (response) => response.organizations + ); } /** * Fetch all user instance locales */ - public async getLocales(fetchAllLocales = true): Promise { - const results = [await this.client.request(`/api/v2/locales`)]; - - if (fetchAllLocales) { - while (true) { - const nextPage = results[results.length - 1].next_page; - - if (!nextPage) { - break; - } - - results.push(await this.client.request(nextPage)); - } - } + public async getLocales(): Promise { + const results = await this.client.request(`/api/v2/locales`); - return results.map(({ locales }) => locales).flat(); + return results.locales; } } From b9083744225bdbfe1fb4fd16eba2d7905e28a101 Mon Sep 17 00:00:00 2001 From: Pol Mampey Date: Thu, 24 Apr 2025 11:23:20 -0400 Subject: [PATCH 5/5] feat(zendesk api): Fix tests --- .../services/zendesk-api-service.spec.ts | 28 +------------------ src/services/zendesk-api-service.ts | 2 +- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/__tests__/services/zendesk-api-service.spec.ts b/__tests__/services/zendesk-api-service.spec.ts index 67f375f..cca8f0a 100644 --- a/__tests__/services/zendesk-api-service.spec.ts +++ b/__tests__/services/zendesk-api-service.spec.ts @@ -420,9 +420,8 @@ describe("ZendeskService", () => { expect(result).toEqual(organizations); }); }); - describe("getLocales", () => { - it("should call the API and return the locales", async () => { + it("should fetch and return locales", async () => { const locales = [{ locale: "en-US" }]; requestMock.mockResolvedValueOnce({ locales }); @@ -431,31 +430,6 @@ describe("ZendeskService", () => { expect(requestMock).toHaveBeenCalledWith(`/api/v2/locales`); expect(result).toEqual(locales); }); - - it("should continue calling the API until next_page disappears", async () => { - const locales = [{ locale: "en-US" }]; - requestMock - .mockResolvedValueOnce({ locales, next_page: "next_page" }) - .mockResolvedValueOnce({ locales: [] }); - - const result = await service.getLocales(); - - expect(requestMock).toHaveBeenCalledTimes(2); - expect(requestMock).toHaveBeenNthCalledWith(1, `/api/v2/locales`); - expect(requestMock).toHaveBeenNthCalledWith(2, "next_page"); - expect(result).toEqual(locales); - }); - - it("should only call the API one time with fetchAllLocales set to false", async () => { - const locales = [{ locale: "en-US" }]; - requestMock.mockResolvedValueOnce({ locales, next_page: "next_page" }); - - const result = await service.getLocales(false); - - expect(requestMock).toHaveBeenCalledTimes(1); - expect(requestMock).toHaveBeenCalledWith(`/api/v2/locales`); - expect(result).toEqual(locales); - }); }); }); }); diff --git a/src/services/zendesk-api-service.ts b/src/services/zendesk-api-service.ts index 539e95c..40a6012 100644 --- a/src/services/zendesk-api-service.ts +++ b/src/services/zendesk-api-service.ts @@ -143,7 +143,7 @@ export class ZendeskApiService { */ public async getUserFields(fetchAllFields = true): Promise { return this.fetchAllPaginatedResults( - `/api/v2/tags`, + `/api/v2/user_fields`, fetchAllFields, (response) => response.user_fields );