Skip to content

Commit 21bf23a

Browse files
author
Michal Nowak
committed
feat(articles): add mapping function for search articles
1 parent 819cbd6 commit 21bf23a

2 files changed

Lines changed: 86 additions & 90 deletions

File tree

packages/integrations/zendesk/src/modules/articles/zendesk-article.mapper.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,40 @@ export function mapCategories(categories: ZendeskCategory[], total: number, loca
356356
}
357357

358358
/**
359-
* Map articles with individual categories per article
360-
* Used for search results where each article may belong to a different category
359+
* Map articles for search results (minimal data - no attachments/authors needed)
360+
* Used when only basic article info with category slugs is required
361+
*/
362+
export function mapSearchArticles(
363+
articles: ZendeskArticle[],
364+
total: number,
365+
locale: string,
366+
categoriesArray: (ZendeskCategory | undefined)[] = [],
367+
): Articles.Model.Articles {
368+
return {
369+
data: articles.map((article, index) => {
370+
const articleSlug = extractSlugFromUrl(article.html_url, article.id);
371+
const category = categoriesArray[index];
372+
const categorySlug = category ? mapCategory(category, locale).slug : undefined;
373+
const fullSlug = categorySlug ? `${categorySlug}/${articleSlug}` : articleSlug;
374+
const lead = extractLeadFromBody(article.body);
375+
376+
return {
377+
id: article.id?.toString() || '',
378+
slug: fullSlug,
379+
createdAt: article.created_at || '',
380+
updatedAt: article.updated_at || '',
381+
title: article.title || '',
382+
lead,
383+
tags: article.label_names || [],
384+
};
385+
}),
386+
total,
387+
};
388+
}
389+
390+
/**
391+
* Map articles with full data (attachments, authors, categories)
392+
* Used for article lists where thumbnails and author info are displayed
361393
*/
362394
export function mapArticlesWithCategories(
363395
articles: ZendeskArticle[],
@@ -371,12 +403,10 @@ export function mapArticlesWithCategories(
371403
data: articles.map((article, index) => {
372404
const articleSlug = extractSlugFromUrl(article.html_url, article.id);
373405
const category = categoriesArray[index];
374-
// Build full slug: /help-and-support/{category-slug}/{article-slug}
375406
const categorySlug = category ? mapCategory(category, locale).slug : undefined;
376407
const fullSlug = categorySlug ? `${categorySlug}/${articleSlug}` : articleSlug;
377408
const lead = extractLeadFromBody(article.body);
378409

379-
// Get attachments for this article
380410
const attachments = attachmentsArray[index] || [];
381411
const inlineImages = attachments.filter(
382412
(att) => att.inline && att.content_type?.startsWith('image/') && att.content_url,
@@ -385,7 +415,6 @@ export function mapArticlesWithCategories(
385415
(att) => !att.inline && att.content_type?.startsWith('image/') && att.content_url,
386416
);
387417

388-
// Use first inline as thumbnail, fallback to first non-inline
389418
const thumbnail = inlineImages[0]
390419
? {
391420
url: inlineImages[0].content_url!,
@@ -398,7 +427,6 @@ export function mapArticlesWithCategories(
398427
}
399428
: undefined;
400429

401-
// Use first non-inline as image, fallback to first inline
402430
const image = nonInlineImages[0]
403431
? {
404432
url: nonInlineImages[0].content_url!,
@@ -411,7 +439,6 @@ export function mapArticlesWithCategories(
411439
}
412440
: undefined;
413441

414-
// Get author for this article
415442
const author = authorsArray[index];
416443

417444
return {

packages/integrations/zendesk/src/modules/articles/zendesk-article.service.ts

Lines changed: 52 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Injectable, NotFoundException } from '@nestjs/common';
2-
import { Observable, catchError, firstValueFrom, from, map, of, switchMap, throwError } from 'rxjs';
2+
import { Observable, catchError, firstValueFrom, forkJoin, from, map, of, switchMap, throwError } from 'rxjs';
33

44
import { Articles } from '@o2s/framework/modules';
55

@@ -22,7 +22,13 @@ import { showUser } from '@/generated/zendesk';
2222
import type { UserObject } from '@/generated/zendesk';
2323
import { client as ticketingClient } from '@/generated/zendesk/client.gen';
2424

25-
import { mapArticle, mapArticlesWithCategories, mapCategories, mapCategory } from './zendesk-article.mapper';
25+
import {
26+
mapArticle,
27+
mapArticlesWithCategories,
28+
mapCategories,
29+
mapCategory,
30+
mapSearchArticles,
31+
} from './zendesk-article.mapper';
2632

2733
type ZendeskArticle = ArticleObject;
2834
type ZendeskCategory = CategoryObject;
@@ -170,60 +176,50 @@ export class ZendeskArticleService extends Articles.Service {
170176
switchMap((response) => {
171177
const articles = response.articles || [];
172178

173-
// Fetch attachments and authors for all articles in parallel
174-
const attachmentsPromises = articles.map((article) =>
175-
firstValueFrom(
176-
this.fetchArticleAttachments(article.id!, zendeskLocale).pipe(catchError(() => of([]))),
177-
),
179+
if (articles.length === 0) {
180+
return of(mapArticlesWithCategories(articles, 0, options.locale, [], [], []));
181+
}
182+
183+
// Fetch attachments, authors, and categories in parallel using forkJoin
184+
const attachments$ = articles.map((article) =>
185+
this.fetchArticleAttachments(article.id!, zendeskLocale).pipe(catchError(() => of([]))),
178186
);
179187

180-
const authorsPromises = articles.map((article) =>
188+
const authors$ = articles.map((article) =>
181189
article.author_id
182-
? firstValueFrom(
183-
this.fetchUser(article.author_id).pipe(catchError(() => of(undefined))),
184-
)
185-
: Promise.resolve(undefined),
190+
? this.fetchUser(article.author_id).pipe(catchError(() => of(undefined)))
191+
: of(undefined),
186192
);
187193

188194
// If category filter is provided, use that category for all articles
189195
// Otherwise, fetch category for each article via section_id -> category_id
190-
const categoriesPromises = category
191-
? articles.map(() => Promise.resolve(category))
196+
const categories$ = category
197+
? articles.map(() => of(category))
192198
: articles.map((article) =>
193199
article.section_id
194-
? firstValueFrom(
195-
this.fetchSection(article.section_id, zendeskLocale).pipe(
196-
switchMap((section) => {
197-
if (!section?.category_id) {
198-
return of(undefined);
199-
}
200-
return this.fetchCategory(section.category_id, zendeskLocale);
201-
}),
202-
catchError(() => of(undefined)),
203-
),
200+
? this.fetchSection(article.section_id, zendeskLocale).pipe(
201+
switchMap((section) => {
202+
if (!section?.category_id) {
203+
return of(undefined);
204+
}
205+
return this.fetchCategory(section.category_id, zendeskLocale);
206+
}),
207+
catchError(() => of(undefined)),
204208
)
205-
: Promise.resolve(undefined),
209+
: of(undefined),
206210
);
207211

208-
return from(
209-
Promise.all([
210-
Promise.all(attachmentsPromises),
211-
Promise.all(authorsPromises),
212-
Promise.all(categoriesPromises),
213-
]),
214-
).pipe(
215-
map(([attachmentsArray, authorsArray, categoriesArray]) => {
216-
// Zendesk doesn't provide total count in the response, so we use articles.length
217-
// In a real scenario, you might need to make additional requests to get the total
218-
return mapArticlesWithCategories(
212+
return forkJoin([forkJoin(attachments$), forkJoin(authors$), forkJoin(categories$)]).pipe(
213+
map(([attachmentsArray, authorsArray, categoriesArray]) =>
214+
mapArticlesWithCategories(
219215
articles,
220216
articles.length,
221217
options.locale,
222218
attachmentsArray,
223219
authorsArray,
224220
categoriesArray,
225-
);
226-
}),
221+
),
222+
),
227223
);
228224
}),
229225
);
@@ -378,56 +374,29 @@ export class ZendeskArticleService extends Articles.Service {
378374
switchMap((response) => {
379375
const articles = response.data?.results || [];
380376

381-
// Fetch attachments, authors, and categories for all articles in parallel
382-
const attachmentsPromises = articles.map((article) =>
383-
firstValueFrom(
384-
this.fetchArticleAttachments(article.id!, zendeskLocale).pipe(catchError(() => of([]))),
385-
),
386-
);
387-
388-
const authorsPromises = articles.map((article) =>
389-
article.author_id
390-
? firstValueFrom(
391-
this.fetchUser(article.author_id).pipe(catchError(() => of(undefined))),
392-
)
393-
: Promise.resolve(undefined),
394-
);
377+
if (articles.length === 0) {
378+
return of(mapSearchArticles(articles, 0, options.locale, []));
379+
}
395380

396-
// Fetch category for each article via section_id -> category_id
397-
// This ensures full slugs even when no category filter is provided
398-
const categoriesPromises = articles.map((article) =>
381+
// Fetch only categories for search results (no attachments/authors needed)
382+
const categories$ = articles.map((article) =>
399383
article.section_id
400-
? firstValueFrom(
401-
this.fetchSection(article.section_id, zendeskLocale).pipe(
402-
switchMap((section) => {
403-
if (!section?.category_id) {
404-
return of(undefined);
405-
}
406-
return this.fetchCategory(section.category_id, zendeskLocale);
407-
}),
408-
catchError(() => of(undefined)),
409-
),
384+
? this.fetchSection(article.section_id, zendeskLocale).pipe(
385+
switchMap((section) => {
386+
if (!section?.category_id) {
387+
return of(undefined);
388+
}
389+
return this.fetchCategory(section.category_id, zendeskLocale);
390+
}),
391+
catchError(() => of(undefined)),
410392
)
411-
: Promise.resolve(undefined),
393+
: of(undefined),
412394
);
413395

414-
return from(
415-
Promise.all([
416-
Promise.all(attachmentsPromises),
417-
Promise.all(authorsPromises),
418-
Promise.all(categoriesPromises),
419-
]),
420-
).pipe(
421-
map(([attachmentsArray, authorsArray, categoriesArray]) => {
422-
return mapArticlesWithCategories(
423-
articles,
424-
articles.length,
425-
options.locale,
426-
attachmentsArray,
427-
authorsArray,
428-
categoriesArray,
429-
);
430-
}),
396+
return forkJoin(categories$).pipe(
397+
map((categoriesArray) =>
398+
mapSearchArticles(articles, articles.length, options.locale, categoriesArray),
399+
),
431400
);
432401
}),
433402
catchError((error) => {

0 commit comments

Comments
 (0)