diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-05-24 19:35:12 +0200 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-05-24 19:35:12 +0200 | 
| commit | c85ab5ad43ccf52881ee224672c41ec30021cf48 (patch) | |
| tree | 8058808d9bfca19383f120c46b34d99ff2f89f63 /src/services/graphql | |
| parent | 52404177c07a2aab7fc894362fb3060dff2431a0 (diff) | |
| parent | 11b9de44a4b2f305a6a484187805e429b2767118 (diff) | |
refactor: use storybook and atomic design (#16)
BREAKING CHANGE: rewrite most of the Typescript types, so the content format (the meta in particular) needs to be updated.
Diffstat (limited to 'src/services/graphql')
| -rw-r--r-- | src/services/graphql/api.ts | 319 | ||||
| -rw-r--r-- | src/services/graphql/articles.query.ts | 191 | ||||
| -rw-r--r-- | src/services/graphql/articles.ts | 200 | ||||
| -rw-r--r-- | src/services/graphql/comments.mutation.ts | 30 | ||||
| -rw-r--r-- | src/services/graphql/comments.query.ts | 21 | ||||
| -rw-r--r-- | src/services/graphql/comments.ts | 102 | ||||
| -rw-r--r-- | src/services/graphql/contact.mutation.ts | 25 | ||||
| -rw-r--r-- | src/services/graphql/contact.ts | 26 | ||||
| -rw-r--r-- | src/services/graphql/mutations.ts | 82 | ||||
| -rw-r--r-- | src/services/graphql/queries.ts | 535 | ||||
| -rw-r--r-- | src/services/graphql/thematics.query.ts | 125 | ||||
| -rw-r--r-- | src/services/graphql/thematics.ts | 162 | ||||
| -rw-r--r-- | src/services/graphql/topics.query.ts | 137 | ||||
| -rw-r--r-- | src/services/graphql/topics.ts | 164 | 
14 files changed, 1486 insertions, 633 deletions
| diff --git a/src/services/graphql/api.ts b/src/services/graphql/api.ts index a5be026..009aea4 100644 --- a/src/services/graphql/api.ts +++ b/src/services/graphql/api.ts @@ -1,25 +1,312 @@ -import { RequestType, VariablesType } from '@ts/types/app';  import { settings } from '@utils/config'; -import { GraphQLClient } from 'graphql-request'; +import { +  articleBySlugQuery, +  articlesCardQuery, +  articlesEndCursor, +  articlesQuery, +  articlesSlugQuery, +  totalArticlesQuery, +} from './articles.query'; +import { sendCommentMutation } from './comments.mutation'; +import { commentsQuery } from './comments.query'; +import { sendMailMutation } from './contact.mutation'; +import { +  thematicBySlugQuery, +  thematicsListQuery, +  thematicsSlugQuery, +  totalThematicsQuery, +} from './thematics.query'; +import { +  topicBySlugQuery, +  topicsListQuery, +  topicsSlugQuery, +  totalTopicsQuery, +} from './topics.query'; -export const getGraphQLClient = (): GraphQLClient => { -  const apiUrl = settings.api.url; +export type Mutations = typeof sendMailMutation | typeof sendCommentMutation; -  if (!apiUrl) throw new Error('API URL not defined.'); +export type Queries = +  | typeof articlesQuery +  | typeof articleBySlugQuery +  | typeof articlesCardQuery +  | typeof articlesEndCursor +  | typeof articlesSlugQuery +  | typeof commentsQuery +  | typeof thematicBySlugQuery +  | typeof thematicsListQuery +  | typeof thematicsSlugQuery +  | typeof topicBySlugQuery +  | typeof topicsListQuery +  | typeof topicsSlugQuery +  | typeof totalArticlesQuery +  | typeof totalThematicsQuery +  | typeof totalTopicsQuery; -  return new GraphQLClient(apiUrl); +export type ArticleResponse<T> = { +  post: T;  }; -export const fetchApi = async <T extends RequestType>( -  query: string, -  variables: VariablesType<T> -): Promise<T> => { -  const client = getGraphQLClient(); +export type ArticlesResponse<T> = { +  posts: T; +}; + +export type CommentsResponse<T> = { +  comments: T; +}; + +export type SendCommentResponse<T> = { +  createComment: T; +}; + +export type SendMailResponse<T> = { +  sendEmail: T; +}; -  try { -    return await client.request(query, variables); -  } catch (error) { -    console.error(error, undefined, 2); -    process.exit(1); +export type ThematicResponse<T> = { +  thematic: T; +}; + +export type ThematicsResponse<T> = { +  thematics: T; +}; + +export type TopicResponse<T> = { +  topic: T; +}; + +export type TopicsResponse<T> = { +  topics: T; +}; + +export type PageInfo = { +  endCursor: string; +  hasNextPage: boolean; +  total: number; +}; + +export type Edges<T> = { +  cursor: string; +  node: T; +}; + +export type EdgesResponse<T> = { +  edges: Edges<T>[]; +  pageInfo: PageInfo; +}; + +export type NodeResponse<T> = { +  node: T; +}; + +export type NodesResponse<T> = { +  nodes: T[]; +}; + +export type EndCursor = Pick< +  EdgesResponse<Pick<PageInfo, 'endCursor'>>, +  'pageInfo' +>; + +export type ResponseMap<T> = { +  [articleBySlugQuery]: ArticleResponse<T>; +  [articlesCardQuery]: ArticlesResponse<NodesResponse<T>>; +  [articlesEndCursor]: ArticlesResponse<EndCursor>; +  [articlesQuery]: ArticlesResponse<EdgesResponse<T>>; +  [articlesSlugQuery]: ArticlesResponse<EdgesResponse<T>>; +  [commentsQuery]: CommentsResponse<NodesResponse<T>>; +  [sendCommentMutation]: SendCommentResponse<T>; +  [sendMailMutation]: SendMailResponse<T>; +  [thematicBySlugQuery]: ThematicResponse<T>; +  [thematicsListQuery]: ThematicsResponse<EdgesResponse<T>>; +  [thematicsSlugQuery]: ThematicsResponse<EdgesResponse<T>>; +  [topicBySlugQuery]: TopicResponse<T>; +  [topicsListQuery]: TopicsResponse<EdgesResponse<T>>; +  [topicsSlugQuery]: TopicsResponse<EdgesResponse<T>>; +  [totalArticlesQuery]: ArticlesResponse<T>; +  [totalThematicsQuery]: ThematicsResponse<T>; +  [totalTopicsQuery]: TopicsResponse<T>; +}; + +export type GraphQLResponse< +  T extends keyof ResponseMap<U>, +  U +> = ResponseMap<U>[T]; + +export type BySlugVar = { +  /** +   * A slug. +   */ +  slug: string; +}; + +export type EdgesVars = { +  /** +   * A cursor. +   */ +  after?: string; +  /** +   * The number of items to return. +   */ +  first: number; +  /** +   * A search query. +   */ +  search?: string; +}; + +export type ByContentIdVar = { +  /** +   * An article id. +   */ +  contentId: number; +}; + +export type SearchVar = { +  /** +   * A search term. +   */ +  search?: string; +}; + +export type SendCommentVars = { +  /** +   * The author name. +   */ +  author: string; +  /** +   * The author e-mail address. +   */ +  authorEmail: string; +  /** +   * The author website. +   */ +  authorUrl: string; +  /** +   * A mutation id. +   */ +  clientMutationId: string; +  /** +   * A post or page id. +   */ +  commentOn: number; +  /** +   * The comment body. +   */ +  content: string; +  /** +   * The comment parent. +   */ +  parent?: number; +}; + +export type SendMailVars = { +  /** +   * The mail body. +   */ +  body: string; +  /** +   * A mutation id. +   */ +  clientMutationId: string; +  /** +   * The reply to e-mail address. +   */ +  replyTo: string; +  /** +   * The mail subject. +   */ +  subject: string; +}; + +export type VariablesMap = { +  [articleBySlugQuery]: BySlugVar; +  [articlesCardQuery]: EdgesVars; +  [articlesEndCursor]: EdgesVars; +  [articlesQuery]: EdgesVars; +  [articlesSlugQuery]: EdgesVars; +  [commentsQuery]: ByContentIdVar; +  [sendCommentMutation]: SendCommentVars; +  [sendMailMutation]: SendMailVars; +  [thematicBySlugQuery]: BySlugVar; +  [thematicsListQuery]: EdgesVars; +  [thematicsSlugQuery]: EdgesVars; +  [topicBySlugQuery]: BySlugVar; +  [topicsListQuery]: EdgesVars; +  [topicsSlugQuery]: EdgesVars; +  [totalArticlesQuery]: SearchVar; +  [totalThematicsQuery]: null; +  [totalTopicsQuery]: null; +}; + +export type FetchAPIProps<T extends Queries | Mutations> = { +  /** +   * A GraphQL API URL. +   */ +  api: string; +  /** +   * A GraphQL query. +   */ +  query: T; +  /** +   * (Optional) The query variables. +   */ +  variables?: VariablesMap[T]; +}; + +/** + * Fetch a GraphQL API. + * @param {object} obj - An object. + * @param {string} obj.api - A GraphQL API URL. + * @param {Queries} obj.query - A GraphQL query. + * @param {object} [obj.variables] - The query variables. + */ +export async function fetchAPI<T, U extends Queries | Mutations>({ +  api, +  query, +  variables, +}: FetchAPIProps<U>): Promise<GraphQLResponse<U, T>> { +  const response = await fetch(api, { +    method: 'POST', +    headers: { +      'content-type': 'application/json;charset=UTF-8', +    }, +    body: JSON.stringify({ +      query, +      variables, +    }), +  }); + +  type JSONResponse = { +    data?: GraphQLResponse<U, T>; +    errors?: Array<{ message: string }>; +  }; + +  const { data, errors }: JSONResponse = await response.json(); + +  if (response.ok) { +    if (!data) return Promise.reject(new Error(`No data found"`)); + +    return data; +  } else { +    console.error('Failed to fetch API'); +    const error = new Error( +      errors?.map((e) => e.message).join('\n') ?? 'unknown' +    ); +    return Promise.reject(error); +  } +} + +/** + * Retrieve the API url from settings. + * + * @returns {string} The API url. + */ +export const getAPIUrl = (): string => { +  const { url } = settings.api; + +  if (!url) { +    throw new Error('API url is not defined.');    } + +  return url;  }; diff --git a/src/services/graphql/articles.query.ts b/src/services/graphql/articles.query.ts new file mode 100644 index 0000000..3e1f575 --- /dev/null +++ b/src/services/graphql/articles.query.ts @@ -0,0 +1,191 @@ +/** + * Query the full article data using its slug. + */ +export const articleBySlugQuery = `query PostBy($slug: ID!) { +  post(id: $slug, idType: SLUG) { +    acfPosts { +      postsInThematic { +        ... on Thematic { +          databaseId +          slug +          title +        } +      } +      postsInTopic { +        ... on Topic { +          databaseId +          featuredImage { +            node { +              altText +              mediaDetails { +                height +                width +              } +              sourceUrl +              title +            } +          } +          slug +          title +        } +      } +    } +    author { +      node { +        gravatarUrl +        name +        url +      } +    } +    commentCount +    contentParts { +      afterMore +      beforeMore +    } +    databaseId +    date +    featuredImage { +      node { +        altText +        mediaDetails { +          height +          width +        } +        sourceUrl +        title +      } +    } +    info { +      wordsCount +    } +    modified +    seo { +      metaDesc +      title +    } +    slug +    title +  } +}`; + +/** + * Query an array of partial articles. + */ +export const articlesQuery = `query Articles($after: String = "", $first: Int = 10, $search: String = "") { +  posts( +    after: $after +    first: $first +    where: {orderby: {field: DATE, order: DESC}, search: $search, status: PUBLISH} +  ) { +    edges { +      cursor +      node { +        acfPosts { +          postsInThematic { +            ... on Thematic { +              databaseId +              slug +              title +            } +          } +        } +        commentCount +        contentParts { +          beforeMore +        } +        databaseId +        date +        featuredImage { +          node { +            altText +            mediaDetails { +              height +              width +            } +            sourceUrl +            title +          } +        } +        info { +          wordsCount +        } +        modified +        slug +        title +      } +    } +    pageInfo { +      endCursor +      hasNextPage +      total +    } +  } +}`; + +/** + * Query an array of articles with only the minimal data. + */ +export const articlesCardQuery = `query ArticlesCard($first: Int = 10) { +  posts( +    first: $first +    where: {orderby: {field: DATE, order: DESC}, status: PUBLISH} +  ) { +    nodes { +      databaseId +      date +      featuredImage { +        node { +          altText +          mediaDetails { +            height +            width +          } +          sourceUrl +          title +        } +      } +      slug +      title +    } +  } +}`; + +/** + * Query an array of articles slug. + */ +export const articlesSlugQuery = `query ArticlesSlug($first: Int = 10, $after: String = "") { +  posts(after: $after, first: $first) { +    edges { +      cursor +      node { +        slug +      } +    } +    pageInfo { +      total +    } +  } +}`; + +/** + * Query the total number of articles. + */ +export const totalArticlesQuery = `query PostsTotal($search: String = "") { +  posts(where: {search: $search}) { +    pageInfo { +      total +    } +  } +}`; + +/** + * Query the end cursor based on the queried posts number. + */ +export const articlesEndCursor = `query EndCursorAfter($first: Int) { +  posts(first: $first) { +    pageInfo { +      hasNextPage +      endCursor +    } +  } +}`; diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts new file mode 100644 index 0000000..27406ac --- /dev/null +++ b/src/services/graphql/articles.ts @@ -0,0 +1,200 @@ +import { Slug, type Article, type ArticleCard } from '@ts/types/app'; +import { +  type RawArticle, +  type RawArticlePreview, +  type TotalItems, +} from '@ts/types/raw-data'; +import { getAuthorFromRawData } from '@utils/helpers/author'; +import { getImageFromRawData } from '@utils/helpers/images'; +import { getPageLinkFromRawData } from '@utils/helpers/pages'; +import { +  EdgesResponse, +  EdgesVars, +  EndCursor, +  fetchAPI, +  getAPIUrl, +  PageInfo, +} from './api'; +import { +  articleBySlugQuery, +  articlesCardQuery, +  articlesEndCursor, +  articlesQuery, +  articlesSlugQuery, +  totalArticlesQuery, +} from './articles.query'; + +/** + * Retrieve the total number of articles. + * + * @returns {Promise<number>} - The articles total number. + */ +export const getTotalArticles = async (search?: string): Promise<number> => { +  const response = await fetchAPI<TotalItems, typeof totalArticlesQuery>({ +    api: getAPIUrl(), +    query: totalArticlesQuery, +    variables: { search }, +  }); + +  return response.posts.pageInfo.total; +}; + +export type GetArticlesReturn = { +  articles: Article[]; +  pageInfo: PageInfo; +}; + +/** + * Convert raw data to an Article object. + * + * @param {RawArticle} data - The page raw data. + * @returns {Article} The page data. + */ +export const getArticleFromRawData = (data: RawArticle): Article => { +  const { +    acfPosts, +    author, +    commentCount, +    contentParts, +    databaseId, +    date, +    featuredImage, +    info, +    modified, +    slug, +    title, +    seo, +  } = data; + +  return { +    content: contentParts.afterMore, +    id: databaseId, +    intro: contentParts.beforeMore, +    meta: { +      author: author && getAuthorFromRawData(author.node, 'page'), +      commentsCount: commentCount || 0, +      cover: featuredImage?.node +        ? getImageFromRawData(featuredImage.node) +        : undefined, +      dates: { publication: date, update: modified }, +      seo: { +        description: seo?.metaDesc || '', +        title: seo?.title || '', +      }, +      thematics: acfPosts.postsInThematic?.map((thematic) => +        getPageLinkFromRawData(thematic, 'thematic') +      ), +      topics: acfPosts.postsInTopic?.map((topic) => +        getPageLinkFromRawData(topic, 'topic') +      ), +      wordsCount: info.wordsCount, +    }, +    slug, +    title, +  }; +}; + +/** + * Retrieve the given number of articles from API. + * + * @param {EdgesVars} props - An object of GraphQL variables. + * @returns {Promise<EdgesResponse<RawArticle>>} The articles data. + */ +export const getArticles = async ( +  props: EdgesVars +): Promise<EdgesResponse<RawArticle>> => { +  const response = await fetchAPI<RawArticle, typeof articlesQuery>({ +    api: getAPIUrl(), +    query: articlesQuery, +    variables: { ...props }, +  }); + +  return response.posts; +}; + +/** + * Convert a raw article preview to an article card. + * + * @param {RawArticlePreview} data - A raw article preview. + * @returns {ArticleCard} An article card. + */ +const getArticleCardFromRawData = (data: RawArticlePreview): ArticleCard => { +  const { databaseId, date, featuredImage, slug, title } = data; + +  return { +    cover: featuredImage ? getImageFromRawData(featuredImage.node) : undefined, +    dates: { publication: date }, +    id: databaseId, +    slug, +    title, +  }; +}; + +/** + * Retrieve the given number of article cards from API. + * + * @param {EdgesVars} obj - An object. + * @param {number} obj.first - The number of articles. + * @returns {Promise<ArticleCard[]>} - The article cards data. + */ +export const getArticlesCard = async ({ +  first, +}: EdgesVars): Promise<ArticleCard[]> => { +  const response = await fetchAPI<RawArticlePreview, typeof articlesCardQuery>({ +    api: getAPIUrl(), +    query: articlesCardQuery, +    variables: { first }, +  }); + +  return response.posts.nodes.map((node) => getArticleCardFromRawData(node)); +}; + +/** + * Retrieve an Article object by slug. + * + * @param {string} slug - The article slug. + * @returns {Promise<Article>} The requested article. + */ +export const getArticleBySlug = async (slug: string): Promise<Article> => { +  const response = await fetchAPI<RawArticle, typeof articleBySlugQuery>({ +    api: getAPIUrl(), +    query: articleBySlugQuery, +    variables: { slug }, +  }); + +  return getArticleFromRawData(response.post); +}; + +/** + * Retrieve all the articles slugs. + * + * @returns {Promise<string[]>} - An array of articles slugs. + */ +export const getAllArticlesSlugs = async (): Promise<string[]> => { +  const totalArticles = await getTotalArticles(); +  const response = await fetchAPI<Slug, typeof articlesSlugQuery>({ +    api: getAPIUrl(), +    query: articlesSlugQuery, +    variables: { first: totalArticles }, +  }); + +  return response.posts.edges.map((edge) => edge.node.slug); +}; + +/** + * Retrieve the last cursor. + * + * @param {EdgesVars} props - An object of GraphQL variables. + * @returns {Promise<string>} - The end cursor. + */ +export const getArticlesEndCursor = async ( +  props: EdgesVars +): Promise<string> => { +  const response = await fetchAPI<EndCursor, typeof articlesEndCursor>({ +    api: getAPIUrl(), +    query: articlesEndCursor, +    variables: { ...props }, +  }); + +  return response.posts.pageInfo.endCursor; +}; diff --git a/src/services/graphql/comments.mutation.ts b/src/services/graphql/comments.mutation.ts new file mode 100644 index 0000000..f52c7e9 --- /dev/null +++ b/src/services/graphql/comments.mutation.ts @@ -0,0 +1,30 @@ +/** + * Send comment mutation. + */ +export const sendCommentMutation = `mutation CreateComment( +  $author: String! +  $authorEmail: String! +  $authorUrl: String! +  $content: String! +  $parent: ID = null +  $commentOn: Int! +  $clientMutationId: String! +) { +  createComment( +    input: { +      author: $author +      authorEmail: $authorEmail +      authorUrl: $authorUrl +      content: $content +      parent: $parent +      commentOn: $commentOn +      clientMutationId: $clientMutationId +    } +  ) { +    clientMutationId +    success +    comment { +      approved +    } +  } +}`; diff --git a/src/services/graphql/comments.query.ts b/src/services/graphql/comments.query.ts new file mode 100644 index 0000000..ef93e89 --- /dev/null +++ b/src/services/graphql/comments.query.ts @@ -0,0 +1,21 @@ +/** + * Query the comments data by post id. + */ +export const commentsQuery = `query CommentsByPostId($contentId: ID!) { +  comments(where: {contentId: $contentId, order: ASC, orderby: COMMENT_DATE}) { +    nodes { +      approved +      author { +        node { +          gravatarUrl +          name +          url +        } +      } +      content +      databaseId +      date +      parentDatabaseId +    } +  } +}`; diff --git a/src/services/graphql/comments.ts b/src/services/graphql/comments.ts new file mode 100644 index 0000000..28ddfd0 --- /dev/null +++ b/src/services/graphql/comments.ts @@ -0,0 +1,102 @@ +import { Comment } from '@ts/types/app'; +import { RawComment } from '@ts/types/raw-data'; +import { getAuthorFromRawData } from '@utils/helpers/author'; +import { fetchAPI, getAPIUrl, SendCommentVars } from './api'; +import { sendCommentMutation } from './comments.mutation'; +import { commentsQuery } from './comments.query'; + +/** + * Create a comments tree with replies. + * + * @param {Comment[]} comments - A flatten comments list. + * @returns {Comment[]} An array of comments with replies. + */ +export const buildCommentsTree = (comments: Comment[]): Comment[] => { +  type CommentsHashTable = { +    [key: string]: Comment; +  }; + +  const hashTable: CommentsHashTable = Object.create(null); +  const commentsTree: Comment[] = []; + +  comments.forEach( +    (comment) => (hashTable[comment.id] = { ...comment, replies: [] }) +  ); + +  comments.forEach((comment) => { +    if (!comment.parentId) { +      commentsTree.push(hashTable[comment.id]); +    } else { +      hashTable[comment.parentId].replies.push(hashTable[comment.id]); +    } +  }); + +  return commentsTree; +}; + +/** + * Convert a comment from RawComment to Comment type. + * + * @param {RawComment} comment - A raw comment. + * @returns {Comment} A formatted comment. + */ +export const getCommentFromRawData = (comment: RawComment): Comment => { +  const { author, databaseId, date, parentDatabaseId, ...data } = comment; + +  return { +    id: databaseId, +    meta: { +      author: getAuthorFromRawData(author.node, 'comment'), +      date, +    }, +    parentId: parentDatabaseId, +    replies: [], +    ...data, +  }; +}; + +/** + * Retrieve a comments list by post id. + * + * @param {number} id - A post id. + * @returns {Promise<Comment[]>} The comments list. + */ +export const getPostComments = async (id: number): Promise<Comment[]> => { +  const response = await fetchAPI<RawComment, typeof commentsQuery>({ +    api: getAPIUrl(), +    query: commentsQuery, +    variables: { contentId: id }, +  }); + +  const comments = response.comments.nodes.map((comment) => +    getCommentFromRawData(comment) +  ); + +  return buildCommentsTree(comments); +}; + +export type SentComment = { +  clientMutationId: string; +  success: boolean; +  comment: { +    approved: boolean; +  } | null; +}; + +/** + * Send a comment using GraphQL API. + * + * @param {SendCommentVars} data - The comment data. + * @returns {Promise<SentEmail>} The mutation response. + */ +export const sendComment = async ( +  data: SendCommentVars +): Promise<SentComment> => { +  const response = await fetchAPI<SentComment, typeof sendCommentMutation>({ +    api: getAPIUrl(), +    query: sendCommentMutation, +    variables: { ...data }, +  }); + +  return response.createComment; +}; diff --git a/src/services/graphql/contact.mutation.ts b/src/services/graphql/contact.mutation.ts new file mode 100644 index 0000000..b82fc07 --- /dev/null +++ b/src/services/graphql/contact.mutation.ts @@ -0,0 +1,25 @@ +/** + * Send mail mutation. + */ +export const sendMailMutation = `mutation SendEmail( +  $subject: String! +  $body: String! +  $replyTo: String! +  $clientMutationId: String! +) { +  sendEmail( +    input: { +      clientMutationId: $clientMutationId +      body: $body +      replyTo: $replyTo +      subject: $subject +    } +  ) { +    clientMutationId +    message +    sent +    origin +    replyTo +    to +  } +}`; diff --git a/src/services/graphql/contact.ts b/src/services/graphql/contact.ts new file mode 100644 index 0000000..00c6ca2 --- /dev/null +++ b/src/services/graphql/contact.ts @@ -0,0 +1,26 @@ +import { fetchAPI, getAPIUrl, SendMailVars } from './api'; +import { sendMailMutation } from './contact.mutation'; + +export type SentEmail = { +  clientMutationId: string; +  message: string; +  origin: string; +  replyTo: string; +  sent: boolean; +}; + +/** + * Send an email using GraphQL API. + * + * @param {sendMailVars} data - The mail data. + * @returns {Promise<SentEmail>} The mutation response. + */ +export const sendMail = async (data: SendMailVars): Promise<SentEmail> => { +  const response = await fetchAPI<SentEmail, typeof sendMailMutation>({ +    api: getAPIUrl(), +    query: sendMailMutation, +    variables: { ...data }, +  }); + +  return response.sendEmail; +}; diff --git a/src/services/graphql/mutations.ts b/src/services/graphql/mutations.ts deleted file mode 100644 index c697835..0000000 --- a/src/services/graphql/mutations.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { CommentData, CreateComment, CreatedComment } from '@ts/types/comments'; -import { ContactData, SendEmail } from '@ts/types/contact'; -import { gql } from 'graphql-request'; -import { fetchApi } from './api'; - -//============================================================================== -// Comment mutation -//============================================================================== - -export const createComment = async ( -  data: CommentData -): Promise<CreatedComment> => { -  const mutation = gql` -    mutation CreateComment( -      $author: String! -      $authorEmail: String! -      $authorUrl: String! -      $content: String! -      $parent: ID! -      $commentOn: Int! -      $mutationId: String! -    ) { -      createComment( -        input: { -          author: $author -          authorEmail: $authorEmail -          authorUrl: $authorUrl -          content: $content -          parent: $parent -          commentOn: $commentOn -          clientMutationId: $mutationId -        } -      ) { -        clientMutationId -        success -        comment { -          approved -        } -      } -    } -  `; - -  const variables = { ...data }; -  const response = await fetchApi<CreateComment>(mutation, variables); - -  return response.createComment; -}; - -//============================================================================== -// Contact mutation -//============================================================================== - -export const sendMail = async (data: ContactData) => { -  const mutation = gql` -    mutation SendEmail( -      $subject: String! -      $body: String! -      $replyTo: String! -      $mutationId: String! -    ) { -      sendEmail( -        input: { -          clientMutationId: $mutationId -          body: $body -          replyTo: $replyTo -          subject: $subject -        } -      ) { -        clientMutationId -        message -        sent -        origin -        replyTo -        to -      } -    } -  `; - -  const variables = { ...data }; -  const response = await fetchApi<SendEmail>(mutation, variables); -  return response.sendEmail; -}; diff --git a/src/services/graphql/queries.ts b/src/services/graphql/queries.ts deleted file mode 100644 index 9caf62b..0000000 --- a/src/services/graphql/queries.ts +++ /dev/null @@ -1,535 +0,0 @@ -import { Slug } from '@ts/types/app'; -import { Article, PostBy, TotalArticles } from '@ts/types/articles'; -import { -  AllPostsSlug, -  LastPostCursor, -  PostsList, -  RawPostsList, -} from '@ts/types/blog'; -import { Comment, CommentsByPostId } from '@ts/types/comments'; -import { -  AllTopics, -  AllTopicsSlug, -  AllThematics, -  AllThematicsSlug, -  Topic, -  TopicBy, -  TopicPreview, -  Thematic, -  ThematicBy, -  ThematicPreview, -} from '@ts/types/taxonomies'; -import { -  getFormattedPost, -  getFormattedPostPreview, -  getFormattedTopic, -  getFormattedThematic, -  getFormattedComments, -  buildCommentsTree, -} from '@utils/helpers/format'; -import { gql } from 'graphql-request'; -import { fetchApi } from './api'; - -//============================================================================== -// Posts list queries -//============================================================================== - -export const getPostsTotal = async (): Promise<number> => { -  const query = gql` -    query PostsTotal { -      posts { -        pageInfo { -          total -        } -      } -    } -  `; - -  const response = await fetchApi<TotalArticles>(query, null); -  return response.posts.pageInfo.total; -}; - -export const getPublishedPosts = async ({ -  first = 10, -  after = '', -  searchQuery = '', -}: { -  first: number; -  after?: string; -  searchQuery?: string; -}): Promise<PostsList> => { -  const query = gql` -    query AllPublishedPosts($first: Int, $after: String, $searchQuery: String) { -      posts( -        after: $after -        first: $first -        where: { -          status: PUBLISH -          orderby: { field: DATE, order: DESC } -          search: $searchQuery -        } -      ) { -        edges { -          cursor -          node { -            acfPosts { -              postsInTopic { -                ... on Topic { -                  databaseId -                  featuredImage { -                    node { -                      altText -                      sourceUrl -                      title -                    } -                  } -                  id -                  slug -                  title -                } -              } -              postsInThematic { -                ... on Thematic { -                  databaseId -                  id -                  slug -                  title -                } -              } -            } -            commentCount -            contentParts { -              beforeMore -            } -            date -            featuredImage { -              node { -                altText -                sourceUrl -                title -              } -            } -            id -            info { -              readingTime -              wordsCount -            } -            databaseId -            modified -            slug -            title -          } -        } -        pageInfo { -          endCursor -          hasNextPage -          total -        } -      } -    } -  `; - -  const variables = { first, after, searchQuery }; -  const response = await fetchApi<RawPostsList>(query, variables); -  const formattedPosts = response.posts.edges.map((post) => { -    return getFormattedPostPreview(post.node); -  }); - -  return { -    posts: formattedPosts, -    pageInfo: response.posts.pageInfo, -  }; -}; - -export const getAllPostsSlug = async (): Promise<Slug[]> => { -  // 10 000 is an arbitrary number that I use for small websites. -  const query = gql` -    query AllPostsSlug { -      posts(first: 10000) { -        nodes { -          slug -        } -      } -    } -  `; - -  const response = await fetchApi<AllPostsSlug>(query, null); -  return response.posts.nodes; -}; - -//============================================================================== -// Single Post query -//============================================================================== - -export const getPostBySlug = async (slug: string): Promise<Article> => { -  const query = gql` -    query PostBySlug($slug: ID!) { -      post(id: $slug, idType: SLUG) { -        acfPosts { -          postsInTopic { -            ... on Topic { -              id -              featuredImage { -                node { -                  altText -                  sourceUrl -                  title -                } -              } -              slug -              title -            } -          } -          postsInThematic { -            ... on Thematic { -              id -              slug -              title -            } -          } -        } -        author { -          node { -            firstName -            lastName -            name -          } -        } -        commentCount -        contentParts { -          afterMore -          beforeMore -        } -        databaseId -        date -        featuredImage { -          node { -            altText -            sourceUrl -            title -          } -        } -        id -        info { -          readingTime -          wordsCount -        } -        modified -        seo { -          metaDesc -          title -        } -        title -      } -    } -  `; -  const variables = { slug }; -  const response = await fetchApi<PostBy>(query, variables); - -  return getFormattedPost(response.post); -}; - -//============================================================================== -// Comments query -//============================================================================== - -export const getCommentsByPostId = async (id: number): Promise<Comment[]> => { -  const query = gql` -    query PostComments($id: ID!) { -      comments(where: { contentId: $id, order: ASC, orderby: COMMENT_DATE }) { -        nodes { -          approved -          author { -            node { -              databaseId -              gravatarUrl -              name -              url -            } -          } -          content -          databaseId -          date -          parentDatabaseId -        } -      } -    } -  `; - -  const variables = { id }; -  const response = await fetchApi<CommentsByPostId>(query, variables); -  const formattedComments = getFormattedComments(response.comments.nodes); - -  return buildCommentsTree(formattedComments); -}; - -//============================================================================== -// Topic query -//============================================================================== - -export const getTopicBySlug = async (slug: string): Promise<Topic> => { -  const query = gql` -    query TopicBySlug($slug: ID!) { -      topic(id: $slug, idType: SLUG) { -        acfTopics { -          officialWebsite -          postsInTopic { -            ... on Post { -              acfPosts { -                postsInTopic { -                  ... on Topic { -                    databaseId -                    featuredImage { -                      node { -                        altText -                        sourceUrl -                        title -                      } -                    } -                    id -                    slug -                    title -                  } -                } -                postsInThematic { -                  ... on Thematic { -                    databaseId -                    id -                    slug -                    title -                  } -                } -              } -              id -              info { -                readingTime -                wordsCount -              } -              commentCount -              contentParts { -                beforeMore -              } -              databaseId -              date -              featuredImage { -                node { -                  altText -                  sourceUrl -                  title -                } -              } -              modified -              slug -              title -            } -          } -        } -        contentParts { -          afterMore -          beforeMore -        } -        databaseId -        date -        featuredImage { -          node { -            altText -            sourceUrl -            title -          } -        } -        id -        info { -          readingTime -          wordsCount -        } -        modified -        seo { -          metaDesc -          title -        } -        title -      } -    } -  `; -  const variables = { slug }; -  const response = await fetchApi<TopicBy>(query, variables); - -  return getFormattedTopic(response.topic); -}; - -export const getAllTopicsSlug = async (): Promise<Slug[]> => { -  // 10 000 is an arbitrary number that I use for small websites. -  const query = gql` -    query AllTopicsSlug { -      topics(first: 10000) { -        nodes { -          slug -        } -      } -    } -  `; -  const response = await fetchApi<AllTopicsSlug>(query, null); -  return response.topics.nodes; -}; - -export const getAllTopics = async (): Promise<TopicPreview[]> => { -  // 10 000 is an arbitrary number that I use for small websites. -  const query = gql` -    query AllTopics { -      topics(first: 10000, where: { orderby: { field: TITLE, order: ASC } }) { -        nodes { -          databaseId -          slug -          title -        } -      } -    } -  `; - -  const response = await fetchApi<AllTopics>(query, null); -  return response.topics.nodes; -}; - -//============================================================================== -// Thematic query -//============================================================================== - -export const getThematicBySlug = async (slug: string): Promise<Thematic> => { -  const query = gql` -    query ThematicBySlug($slug: ID!) { -      thematic(id: $slug, idType: SLUG) { -        acfThematics { -          postsInThematic { -            ... on Post { -              acfPosts { -                postsInTopic { -                  ... on Topic { -                    databaseId -                    featuredImage { -                      node { -                        altText -                        sourceUrl -                        title -                      } -                    } -                    id -                    slug -                    title -                  } -                } -                postsInThematic { -                  ... on Thematic { -                    databaseId -                    id -                    slug -                    title -                  } -                } -              } -              id -              info { -                readingTime -                wordsCount -              } -              commentCount -              contentParts { -                beforeMore -              } -              databaseId -              date -              featuredImage { -                node { -                  altText -                  sourceUrl -                  title -                } -              } -              modified -              slug -              title -            } -          } -        } -        contentParts { -          afterMore -          beforeMore -        } -        databaseId -        date -        id -        info { -          readingTime -          wordsCount -        } -        modified -        seo { -          metaDesc -          title -        } -        title -      } -    } -  `; -  const variables = { slug }; -  const response = await fetchApi<ThematicBy>(query, variables); - -  return getFormattedThematic(response.thematic); -}; - -export const getAllThematicsSlug = async (): Promise<Slug[]> => { -  // 10 000 is an arbitrary number that I use for small websites. -  const query = gql` -    query AllThematicsSlug { -      thematics(first: 10000) { -        nodes { -          slug -        } -      } -    } -  `; -  const response = await fetchApi<AllThematicsSlug>(query, null); -  return response.thematics.nodes; -}; - -export const getAllThematics = async (): Promise<ThematicPreview[]> => { -  // 10 000 is an arbitrary number that I use for small websites. -  const query = gql` -    query AllThematics { -      thematics( -        first: 10000 -        where: { orderby: { field: TITLE, order: ASC } } -      ) { -        nodes { -          databaseId -          slug -          title -        } -      } -    } -  `; - -  const response = await fetchApi<AllThematics>(query, null); -  return response.thematics.nodes; -}; - -export const getEndCursor = async ({ -  first = 10, -}: { -  first: number; -}): Promise<string> => { -  const query = gql` -    query EndCursorAfter($first: Int) { -      posts(first: $first) { -        pageInfo { -          hasNextPage -          endCursor -        } -      } -    } -  `; - -  const variables = { first }; -  const response = await fetchApi<LastPostCursor>(query, variables); - -  return response.posts.pageInfo.endCursor; -}; diff --git a/src/services/graphql/thematics.query.ts b/src/services/graphql/thematics.query.ts new file mode 100644 index 0000000..5a82133 --- /dev/null +++ b/src/services/graphql/thematics.query.ts @@ -0,0 +1,125 @@ +/** + * Query the full thematic data using its slug. + */ +export const thematicBySlugQuery = `query ThematicBy($slug: ID!) { +  thematic(id: $slug, idType: SLUG) { +    acfThematics { +      postsInThematic { +        ... on Post { +          acfPosts { +            postsInTopic { +              ... on Topic { +                databaseId +                slug +                title +              } +            } +          } +          commentCount +          contentParts { +            beforeMore +          } +          databaseId +          date +          featuredImage { +            node { +              altText +              mediaDetails { +                height +                width +              } +              sourceUrl +              title +            } +          } +          info { +            wordsCount +          } +          modified +          slug +          title +        } +      } +    } +    contentParts { +      afterMore +      beforeMore +    } +    databaseId +    date +    featuredImage { +      node { +        altText +        mediaDetails { +          height +          width +        } +        sourceUrl +        title +      } +    } +    info { +      wordsCount +    } +    modified +    seo { +      metaDesc +      title +    } +    slug +    title +  } +}`; + +/** + * Query an array of partial thematics. + */ +export const thematicsListQuery = `query ThematicsList($after: String = "", $first: Int = 10) { +  thematics( +    after: $after +    first: $first +    where: {orderby: {field: TITLE, order: ASC}, status: PUBLISH} +  ) { +    edges { +      cursor +      node { +        databaseId +        slug +        title +      } +    } +    pageInfo { +      endCursor +      hasNextPage +      total +    } +  } +}`; + +/** + * Query an array of thematics slug. + */ +export const thematicsSlugQuery = `query ThematicsSlug($first: Int = 10, $after: String = "") { +  thematics(after: $after, first: $first) { +    edges { +      cursor +      node { +        slug +      } +    } +    pageInfo { +      total +    } +  } +}`; + +/** + * Query the total number of thematics. + */ +export const totalThematicsQuery = `query ThematicsTotal { +  thematics { +    pageInfo { +      total +    } +  } +}`; diff --git a/src/services/graphql/thematics.ts b/src/services/graphql/thematics.ts new file mode 100644 index 0000000..4dc69e7 --- /dev/null +++ b/src/services/graphql/thematics.ts @@ -0,0 +1,162 @@ +import { PageLink, Slug, Thematic } from '@ts/types/app'; +import { +  RawArticle, +  RawThematic, +  RawThematicPreview, +  TotalItems, +} from '@ts/types/raw-data'; +import { getImageFromRawData } from '@utils/helpers/images'; +import { getPageLinkFromRawData } from '@utils/helpers/pages'; +import { EdgesResponse, EdgesVars, fetchAPI, getAPIUrl } from './api'; +import { getArticleFromRawData } from './articles'; +import { +  thematicBySlugQuery, +  thematicsListQuery, +  thematicsSlugQuery, +  totalThematicsQuery, +} from './thematics.query'; + +/** + * Retrieve the total number of thematics. + * + * @returns {Promise<number>} - The thematics total number. + */ +export const getTotalThematics = async (): Promise<number> => { +  const response = await fetchAPI<TotalItems, typeof totalThematicsQuery>({ +    api: getAPIUrl(), +    query: totalThematicsQuery, +  }); + +  return response.thematics.pageInfo.total; +}; + +/** + * Retrieve the given number of thematics from API. + * + * @param {EdgesVars} props - An object of GraphQL variables. + * @returns {Promise<EdgesResponse<RawThematicPreview>>} The thematics data. + */ +export const getThematicsPreview = async ( +  props: EdgesVars +): Promise<EdgesResponse<RawThematicPreview>> => { +  const response = await fetchAPI< +    RawThematicPreview, +    typeof thematicsListQuery +  >({ api: getAPIUrl(), query: thematicsListQuery, variables: props }); + +  return response.thematics; +}; + +/** + * Convert raw data to an Thematic object. + * + * @param {RawThematic} data - The page raw data. + * @returns {Thematic} The page data. + */ +export const getThematicFromRawData = (data: RawThematic): Thematic => { +  const { +    acfThematics, +    contentParts, +    databaseId, +    date, +    featuredImage, +    info, +    modified, +    slug, +    title, +    seo, +  } = data; + +  /** +   * Retrieve an array of related topics. +   * +   * @param posts - The thematic posts. +   * @returns {PageLink[]} An array of topics links. +   */ +  const getRelatedTopics = (posts: RawArticle[]): PageLink[] => { +    const topics: PageLink[] = []; + +    posts.forEach((post) => { +      if (post.acfPosts.postsInTopic) { +        post.acfPosts.postsInTopic.forEach((topic) => +          topics.push(getPageLinkFromRawData(topic, 'topic')) +        ); +      } +    }); + +    const topicsIds = topics.map((topic) => topic.id); +    const uniqueTopics = topics.filter( +      ({ id }, index) => !topicsIds.includes(id, index + 1) +    ); +    const sortTopicByName = (a: PageLink, b: PageLink) => { +      var nameA = a.name.toUpperCase(); // ignore upper and lowercase +      var nameB = b.name.toUpperCase(); // ignore upper and lowercase +      if (nameA < nameB) { +        return -1; +      } +      if (nameA > nameB) { +        return 1; +      } + +      // names must be equal +      return 0; +    }; + +    return uniqueTopics.sort(sortTopicByName); +  }; + +  return { +    content: contentParts.afterMore, +    id: databaseId, +    intro: contentParts.beforeMore, +    meta: { +      articles: acfThematics.postsInThematic.map((post) => +        getArticleFromRawData(post) +      ), +      cover: featuredImage?.node +        ? getImageFromRawData(featuredImage.node) +        : undefined, +      dates: { publication: date, update: modified }, +      seo: { +        description: seo?.metaDesc || '', +        title: seo?.title || '', +      }, +      topics: getRelatedTopics(acfThematics.postsInThematic), +      wordsCount: info.wordsCount, +    }, +    slug, +    title, +  }; +}; + +/** + * Retrieve a Thematic object by slug. + * + * @param {string} slug - The thematic slug. + * @returns {Promise<Article>} The requested thematic. + */ +export const getThematicBySlug = async (slug: string): Promise<Thematic> => { +  const response = await fetchAPI<RawThematic, typeof thematicBySlugQuery>({ +    api: getAPIUrl(), +    query: thematicBySlugQuery, +    variables: { slug }, +  }); + +  return getThematicFromRawData(response.thematic); +}; + +/** + * Retrieve all the thematics slugs. + * + * @returns {Promise<string[]>} - An array of thematics slugs. + */ +export const getAllThematicsSlugs = async (): Promise<string[]> => { +  const totalThematics = await getTotalThematics(); +  const response = await fetchAPI<Slug, typeof thematicsSlugQuery>({ +    api: getAPIUrl(), +    query: thematicsSlugQuery, +    variables: { first: totalThematics }, +  }); + +  return response.thematics.edges.map((edge) => edge.node.slug); +}; diff --git a/src/services/graphql/topics.query.ts b/src/services/graphql/topics.query.ts new file mode 100644 index 0000000..57b2569 --- /dev/null +++ b/src/services/graphql/topics.query.ts @@ -0,0 +1,137 @@ +/** + * Query the full topic data using its slug. + */ +export const topicBySlugQuery = `query TopicBy($slug: ID!) { +  topic(id: $slug, idType: SLUG) { +    acfTopics { +      officialWebsite +      postsInTopic { +        ... on Post { +          acfPosts { +            postsInThematic { +              ... on Thematic { +                databaseId +                slug +                title +              } +            } +          } +          commentCount +          contentParts { +            beforeMore +          } +          databaseId +          date +          featuredImage { +            node { +              altText +              mediaDetails { +                height +                width +              } +              sourceUrl +              title +            } +          } +          info { +            wordsCount +          } +          modified +          slug +          title +        } +      } +    } +    contentParts { +      afterMore +      beforeMore +    } +    databaseId +    date +    featuredImage { +      node { +        altText +        mediaDetails { +          height +          width +        } +        sourceUrl +        title +      } +    } +    info { +      wordsCount +    } +    modified +    seo { +      metaDesc +      title +    } +    slug +    title +  } +}`; + +/** + * Query an array of partial topics. + */ +export const topicsListQuery = `query TopicsList($after: String = "", $first: Int = 10) { +  topics( +    after: $after +    first: $first +    where: {orderby: {field: TITLE, order: ASC}, status: PUBLISH} +  ) { +    edges { +      cursor +      node { +        databaseId +        featuredImage { +          node { +            altText +            mediaDetails { +              height +              width +            } +            sourceUrl +            title +          } +        } +        slug +        title +      } +    } +    pageInfo { +      endCursor +      hasNextPage +      total +    } +  } +}`; + +/** + * Query an array of topics slug. + */ +export const topicsSlugQuery = `query TopicsSlug($first: Int = 10, $after: String = "") { +  topics(after: $after, first: $first) { +    edges { +      cursor +      node { +        slug +      } +    } +    pageInfo { +      total +    } +  } +}`; + +/** + * Query the total number of topics. + */ +export const totalTopicsQuery = `query TopicsTotal { +  topics { +    pageInfo { +      total +    } +  } +}`; diff --git a/src/services/graphql/topics.ts b/src/services/graphql/topics.ts new file mode 100644 index 0000000..0b1971b --- /dev/null +++ b/src/services/graphql/topics.ts @@ -0,0 +1,164 @@ +import { PageLink, Slug, Topic } from '@ts/types/app'; +import { +  RawArticle, +  RawTopic, +  RawTopicPreview, +  TotalItems, +} from '@ts/types/raw-data'; +import { getImageFromRawData } from '@utils/helpers/images'; +import { getPageLinkFromRawData } from '@utils/helpers/pages'; +import { EdgesResponse, EdgesVars, fetchAPI, getAPIUrl } from './api'; +import { getArticleFromRawData } from './articles'; +import { +  topicBySlugQuery, +  topicsListQuery, +  topicsSlugQuery, +  totalTopicsQuery, +} from './topics.query'; + +/** + * Retrieve the total number of topics. + * + * @returns {Promise<number>} - The topics total number. + */ +export const getTotalTopics = async (): Promise<number> => { +  const response = await fetchAPI<TotalItems, typeof totalTopicsQuery>({ +    api: getAPIUrl(), +    query: totalTopicsQuery, +  }); + +  return response.topics.pageInfo.total; +}; + +/** + * Retrieve the given number of topics from API. + * + * @param {EdgesVars} props - An object of GraphQL variables. + * @returns {Promise<EdgesResponse<RawTopicPreview>>} The topics data. + */ +export const getTopicsPreview = async ( +  props: EdgesVars +): Promise<EdgesResponse<RawTopicPreview>> => { +  const response = await fetchAPI<RawTopicPreview, typeof topicsListQuery>({ +    api: getAPIUrl(), +    query: topicsListQuery, +    variables: props, +  }); + +  return response.topics; +}; + +/** + * Convert raw data to a Topic object. + * + * @param {RawTopic} data - The page raw data. + * @returns {Topic} The page data. + */ +export const getTopicFromRawData = (data: RawTopic): Topic => { +  const { +    acfTopics, +    contentParts, +    databaseId, +    date, +    featuredImage, +    info, +    modified, +    slug, +    title, +    seo, +  } = data; + +  /** +   * Retrieve an array of related topics. +   * +   * @param posts - The topic posts. +   * @returns {PageLink[]} An array of topics links. +   */ +  const getRelatedThematics = (posts: RawArticle[]): PageLink[] => { +    const thematics: PageLink[] = []; + +    posts.forEach((post) => { +      if (post.acfPosts.postsInThematic) { +        post.acfPosts.postsInThematic.forEach((thematic) => +          thematics.push(getPageLinkFromRawData(thematic, 'thematic')) +        ); +      } +    }); + +    const thematicsIds = thematics.map((thematic) => thematic.id); +    const uniqueThematics = thematics.filter( +      ({ id }, index) => !thematicsIds.includes(id, index + 1) +    ); +    const sortThematicByName = (a: PageLink, b: PageLink) => { +      var nameA = a.name.toUpperCase(); // ignore upper and lowercase +      var nameB = b.name.toUpperCase(); // ignore upper and lowercase +      if (nameA < nameB) { +        return -1; +      } +      if (nameA > nameB) { +        return 1; +      } + +      // names must be equal +      return 0; +    }; + +    return uniqueThematics.sort(sortThematicByName); +  }; + +  return { +    content: contentParts.afterMore, +    id: databaseId, +    intro: contentParts.beforeMore, +    meta: { +      articles: acfTopics.postsInTopic.map((post) => +        getArticleFromRawData(post) +      ), +      cover: featuredImage?.node +        ? getImageFromRawData(featuredImage.node) +        : undefined, +      dates: { publication: date, update: modified }, +      website: acfTopics.officialWebsite, +      seo: { +        description: seo?.metaDesc || '', +        title: seo?.title || '', +      }, +      thematics: getRelatedThematics(acfTopics.postsInTopic), +      wordsCount: info.wordsCount, +    }, +    slug, +    title, +  }; +}; + +/** + * Retrieve a Topic object by slug. + * + * @param {string} slug - The topic slug. + * @returns {Promise<Article>} The requested topic. + */ +export const getTopicBySlug = async (slug: string): Promise<Topic> => { +  const response = await fetchAPI<RawTopic, typeof topicBySlugQuery>({ +    api: getAPIUrl(), +    query: topicBySlugQuery, +    variables: { slug }, +  }); + +  return getTopicFromRawData(response.topic); +}; + +/** + * Retrieve all the topics slugs. + * + * @returns {Promise<string[]>} - An array of topics slugs. + */ +export const getAllTopicsSlugs = async (): Promise<string[]> => { +  const totalTopics = await getTotalTopics(); +  const response = await fetchAPI<Slug, typeof topicsSlugQuery>({ +    api: getAPIUrl(), +    query: topicsSlugQuery, +    variables: { first: totalTopics }, +  }); + +  return response.topics.edges.map((edge) => edge.node.slug); +}; | 
