aboutsummaryrefslogtreecommitdiffstats
path: root/src/services
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-24 20:00:08 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-27 14:47:51 +0100
commitf111685c5886f3e77edfd3621c98d8ac1b9bcce4 (patch)
tree62a541fe3afeb64bf745443706fbfb02e96c5230 /src/services
parentbee515641cb144be9a855ff2cac258d2fedab21d (diff)
refactor(services, types): reorganize GraphQL fetchers and data types
The Typescript mapped types was useful for autocompletion in fetchers but their are harder to maintain. I think it's better to keep each query close to its fetcher to have a better understanding of the fetched data. So I: * colocate queries with their own fetcher * colocate mutations with their own mutator * remove Typescript mapped types for queries and mutations * move data convertors inside graphql services * rename most of data types and fetchers
Diffstat (limited to 'src/services')
-rw-r--r--src/services/graphql/api.ts83
-rw-r--r--src/services/graphql/articles.query.ts191
-rw-r--r--src/services/graphql/articles.ts201
-rw-r--r--src/services/graphql/comments.mutation.ts30
-rw-r--r--src/services/graphql/comments.query.ts32
-rw-r--r--src/services/graphql/comments.ts177
-rw-r--r--src/services/graphql/contact.mutation.ts25
-rw-r--r--src/services/graphql/contact.ts26
-rw-r--r--src/services/graphql/fetchers/comments/fetch-comments.ts65
-rw-r--r--src/services/graphql/fetchers/comments/index.ts1
-rw-r--r--src/services/graphql/fetchers/index.ts4
-rw-r--r--src/services/graphql/fetchers/posts/fetch-all-posts-slugs.ts34
-rw-r--r--src/services/graphql/fetchers/posts/fetch-last-post-cursor.ts37
-rw-r--r--src/services/graphql/fetchers/posts/fetch-post.ts92
-rw-r--r--src/services/graphql/fetchers/posts/fetch-posts-count.ts43
-rw-r--r--src/services/graphql/fetchers/posts/fetch-posts-list.ts97
-rw-r--r--src/services/graphql/fetchers/posts/fetch-recent-posts.ts76
-rw-r--r--src/services/graphql/fetchers/posts/index.ts6
-rw-r--r--src/services/graphql/fetchers/thematics/fetch-all-thematics-slugs.ts34
-rw-r--r--src/services/graphql/fetchers/thematics/fetch-thematic.ts96
-rw-r--r--src/services/graphql/fetchers/thematics/fetch-thematics-count.ts43
-rw-r--r--src/services/graphql/fetchers/thematics/fetch-thematics-list.ts78
-rw-r--r--src/services/graphql/fetchers/thematics/index.ts4
-rw-r--r--src/services/graphql/fetchers/topics/fetch-all-topics-slugs.ts34
-rw-r--r--src/services/graphql/fetchers/topics/fetch-topic.ts97
-rw-r--r--src/services/graphql/fetchers/topics/fetch-topics-count.ts43
-rw-r--r--src/services/graphql/fetchers/topics/fetch-topics-list.ts84
-rw-r--r--src/services/graphql/fetchers/topics/index.ts4
-rw-r--r--src/services/graphql/helpers/build-comments-tree.test.ts67
-rw-r--r--src/services/graphql/helpers/build-comments-tree.ts30
-rw-r--r--src/services/graphql/helpers/convert-post-preview-to-article-preview.test.ts130
-rw-r--r--src/services/graphql/helpers/convert-post-preview-to-article-preview.ts36
-rw-r--r--src/services/graphql/helpers/convert-post-to-article.test.ts125
-rw-r--r--src/services/graphql/helpers/convert-post-to-article.ts43
-rw-r--r--src/services/graphql/helpers/convert-recent-post-to-recent-article.test.ts48
-rw-r--r--src/services/graphql/helpers/convert-recent-post-to-recent-article.ts24
-rw-r--r--src/services/graphql/helpers/convert-taxonomy-to-page-link.test.ts52
-rw-r--r--src/services/graphql/helpers/convert-taxonomy-to-page-link.ts23
-rw-r--r--src/services/graphql/helpers/convert-wp-comment-to-comment.test.ts93
-rw-r--r--src/services/graphql/helpers/convert-wp-comment-to-comment.ts35
-rw-r--r--src/services/graphql/helpers/convert-wp-image-to-img.test.ts41
-rw-r--r--src/services/graphql/helpers/convert-wp-image-to-img.ts16
-rw-r--r--src/services/graphql/helpers/index.ts7
-rw-r--r--src/services/graphql/index.ts15
-rw-r--r--src/services/graphql/mutators/create-comment.ts70
-rw-r--r--src/services/graphql/mutators/index.ts2
-rw-r--r--src/services/graphql/mutators/send-email.ts49
-rw-r--r--src/services/graphql/thematics.query.ts125
-rw-r--r--src/services/graphql/thematics.ts157
-rw-r--r--src/services/graphql/topics.query.ts137
-rw-r--r--src/services/graphql/topics.ts154
51 files changed, 1866 insertions, 1350 deletions
diff --git a/src/services/graphql/api.ts b/src/services/graphql/api.ts
deleted file mode 100644
index 003f92d..0000000
--- a/src/services/graphql/api.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import type {
- Mutations,
- MutationsInputMap,
- MutationsResponseMap,
- Queries,
- QueriesInputMap,
- QueriesResponseMap,
-} from '../../types';
-import { CONFIG } from '../../utils/config';
-
-/**
- * Retrieve the API url from settings.
- *
- * @returns {string} The API url.
- */
-export const getAPIUrl = (): string => {
- const { url } = CONFIG.api;
-
- if (!url) {
- throw new Error('API url is not defined.');
- }
-
- return url;
-};
-
-export type ResponseMap<T, K extends Mutations | Queries> = K extends Mutations
- ? MutationsResponseMap<T>
- : QueriesResponseMap<T>;
-
-export type InputMap<T extends Mutations | Queries> = T extends Mutations
- ? MutationsInputMap
- : QueriesInputMap;
-
-type FetchAPIVariables<T> = T extends Queries
- ? QueriesInputMap[T]
- : T extends Mutations
- ? MutationsInputMap[T]
- : never;
-
-type FetchAPIProps<Q extends Queries | Mutations, V = FetchAPIVariables<Q>> = {
- query: Q;
- variables?: V;
-};
-
-type FetchAPIResponse<T, K extends Queries | Mutations> = K extends Queries
- ? QueriesResponseMap<T>[K]
- : K extends Mutations
- ? MutationsResponseMap<T>[K]
- : never;
-
-export const fetchAPI = async <T, K extends Queries | Mutations>({
- query,
- variables,
-}: FetchAPIProps<K>): Promise<FetchAPIResponse<T, K>> => {
- const response = await fetch(getAPIUrl(), {
- method: 'POST',
- headers: {
- 'content-type': 'application/json;charset=UTF-8',
- },
- body: JSON.stringify({
- query,
- variables,
- }),
- });
-
- type JSONResponse = {
- data?: FetchAPIResponse<T, K>;
- errors?: { message: string }[];
- };
-
- const { data, errors }: JSONResponse = await response.json();
-
- if (response.ok) {
- if (!data) return Promise.reject(new Error(`No data found"`));
-
- return data;
- }
- console.error('Failed to fetch API');
- const error = new Error(
- errors?.map((e) => e.message).join('\n') ?? 'unknown'
- );
- return Promise.reject(error);
-};
diff --git a/src/services/graphql/articles.query.ts b/src/services/graphql/articles.query.ts
deleted file mode 100644
index 46e3df6..0000000
--- a/src/services/graphql/articles.query.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-/**
- * 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 articlesEndCursorQuery = `query EndCursorAfter($first: Int) {
- posts(first: $first) {
- pageInfo {
- hasNextPage
- endCursor
- }
- }
-}`;
diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts
deleted file mode 100644
index 82bde41..0000000
--- a/src/services/graphql/articles.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import type {
- Article,
- ArticleCard,
- EdgesResponse,
- EndCursorResponse,
- GraphQLEdgesInput,
- GraphQLPageInfo,
- RawArticle,
- RawArticlePreview,
- Slug,
- TotalItems,
-} from '../../types';
-import {
- getAuthorFromRawData,
- getImageFromRawData,
- getPageLinkFromRawData,
- updateContentTree,
-} from '../../utils/helpers';
-import { fetchAPI } from './api';
-import {
- articleBySlugQuery,
- articlesCardQuery,
- articlesEndCursorQuery,
- 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>({
- query: totalArticlesQuery,
- variables: { search },
- });
-
- return response.posts.pageInfo.total;
-};
-
-export type GetArticlesReturn = {
- articles: Article[];
- pageInfo: GraphQLPageInfo;
-};
-
-/**
- * Convert raw data to an Article object.
- *
- * @param {RawArticle} data - The page raw data.
- * @returns {Article} The page data.
- */
-export const getArticleFromRawData = async (
- data: RawArticle
-): Promise<Article> => {
- const {
- acfPosts,
- author,
- commentCount,
- contentParts,
- databaseId,
- date,
- featuredImage,
- info,
- modified,
- slug,
- title,
- seo,
- } = data;
-
- return {
- content: await updateContentTree(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 {GraphQLEdgesInput} props - An object of GraphQL variables.
- * @returns {Promise<EdgesResponse<RawArticle>>} The articles data.
- */
-export const getArticles = async (
- props: GraphQLEdgesInput
-): Promise<EdgesResponse<RawArticle>> => {
- const response = await fetchAPI<RawArticle, typeof articlesQuery>({
- 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 {GraphQLEdgesInput} obj - An object.
- * @param {number} obj.first - The number of articles.
- * @returns {Promise<ArticleCard[]>} - The article cards data.
- */
-export const getArticlesCard = async ({
- first,
-}: GraphQLEdgesInput): Promise<ArticleCard[]> => {
- const response = await fetchAPI<RawArticlePreview, typeof articlesCardQuery>({
- 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>({
- 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>({
- query: articlesSlugQuery,
- variables: { first: totalArticles },
- });
-
- return response.posts.edges.map((edge) => edge.node.slug);
-};
-
-/**
- * Retrieve the last cursor.
- *
- * @param {GraphQLEdgesInput} props - An object of GraphQL variables.
- * @returns {Promise<string>} - The end cursor.
- */
-export const getArticlesEndCursor = async (
- props: GraphQLEdgesInput
-): Promise<string> => {
- const response = await fetchAPI<
- EndCursorResponse,
- typeof articlesEndCursorQuery
- >({
- query: articlesEndCursorQuery,
- variables: { ...props },
- });
-
- return response.posts.pageInfo.endCursor;
-};
diff --git a/src/services/graphql/comments.mutation.ts b/src/services/graphql/comments.mutation.ts
deleted file mode 100644
index f52c7e9..0000000
--- a/src/services/graphql/comments.mutation.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * 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
deleted file mode 100644
index 5110db3..0000000
--- a/src/services/graphql/comments.query.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Query the comments data by post id.
- */
-export const commentsQuery = `query CommentsByPostId($contentId: ID!, $first: Int = 10, $after: String = "") {
- comments(
- where: {contentId: $contentId}
- first: $first
- after: $after
- ) {
- edges {
- cursor
- node {
- approved
- author {
- node {
- gravatarUrl
- name
- url
- }
- }
- content
- databaseId
- date
- parentDatabaseId
- }
- }
- pageInfo {
- hasNextPage
- endCursor
- }
- }
-}`;
diff --git a/src/services/graphql/comments.ts b/src/services/graphql/comments.ts
deleted file mode 100644
index 4eaeac7..0000000
--- a/src/services/graphql/comments.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import {
- type ContentId,
- type GraphQLEdgesInput,
- type RawComment,
- type RawCommentsPage,
- type SendCommentInput,
- type SentComment,
- type SingleComment,
-} from '../../types';
-import { getAuthorFromRawData } from '../../utils/helpers';
-import { fetchAPI } from './api';
-import { sendCommentMutation } from './comments.mutation';
-import { commentsQuery } from './comments.query';
-
-/**
- * Convert a comment from RawComment type to SingleComment type.
- *
- * @param {RawComment} comment - A raw comment.
- * @returns {SingleComment} A formatted comment.
- */
-export const getCommentFromRawData = (comment: RawComment): SingleComment => {
- const { author, databaseId, date, parentDatabaseId, ...data } = comment;
-
- return {
- id: databaseId,
- meta: {
- author: getAuthorFromRawData(author.node, 'comment'),
- date,
- },
- parentId: parentDatabaseId === 0 ? undefined : parentDatabaseId,
- replies: [],
- ...data,
- };
-};
-
-/**
- * Convert an array of RawComment type to an array of SingleComment type.
- *
- * @param {RawComment[]} comments - The raw comments.
- * @returns {SingleComment[]} The formatted comments.
- */
-export const getCommentsFromRawData = (
- comments: RawComment[]
-): SingleComment[] => {
- return comments.map((comment) => getCommentFromRawData(comment));
-};
-
-/**
- * Create a comments tree with replies.
- *
- * @param {SingleComment[]} comments - A flatten comments list.
- * @returns {SingleComment[]} An array of comments with replies.
- */
-export const buildCommentsTree = (
- comments: SingleComment[]
-): SingleComment[] => {
- type CommentsHashTable = {
- [key: string]: SingleComment;
- };
-
- const hashTable: CommentsHashTable = Object.create(null);
- const commentsTree: SingleComment[] = [];
-
- 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;
-};
-
-type FetchCommentsInput = ContentId &
- Pick<GraphQLEdgesInput, 'after' | 'first'>;
-
-/**
- * Retrieve a raw comments page from GraphQL.
- *
- * @param {FetchCommentsInput} variables - An object of variables.
- * @returns {Promise<RawCommentsPage>} A raw comments page.
- */
-export const fetchRawComments = async (
- variables: FetchCommentsInput
-): Promise<RawCommentsPage> => {
- const response = await fetchAPI<RawComment, typeof commentsQuery>({
- query: commentsQuery,
- variables,
- });
-
- return {
- comments: response.comments.edges.map((edge) => edge.node),
- hasNextPage: response.comments.pageInfo.hasNextPage,
- endCursor: response.comments.pageInfo.endCursor,
- };
-};
-
-/**
- * Fetch recursively all the comments on a post.
- *
- * @param {FetchCommentsInput} variables - An object of query variables.
- * @param {RawCommentsPage[]} pages - An accumulator to keep track of pages.
- * @returns {Promise<RawCommentsPage[]>} The raw comments pages.
- */
-export const fetchAllRawCommentsPages = async (
- variables: FetchCommentsInput,
- pages: RawCommentsPage[] = []
-): Promise<RawCommentsPage[]> => {
- return fetchRawComments(variables).then((page) => {
- pages.push(page);
-
- if (page.hasNextPage) {
- return fetchAllRawCommentsPages(
- { ...variables, after: page.endCursor },
- pages
- );
- } else {
- return pages;
- }
- });
-};
-
-/**
- * Method to compare two comments dates and sort them from older to newest.
- *
- * @param {SingleComment} a - A comment.
- * @param {SingleComment} b - Another comment.
- * @returns {number} The difference between dates.
- */
-export const compareCommentsDate = (
- a: SingleComment,
- b: SingleComment
-): number => {
- return +new Date(a.meta.date) - +new Date(b.meta.date);
-};
-
-/**
- * Retrieve all the comments on a post.
- *
- * @param {number} id - A post id.
- * @returns {Promise<SingleComment[]>} The comments list.
- */
-export const getAllComments = async ({
- contentId,
-}: {
- contentId: number;
-}): Promise<SingleComment[]> => {
- const pages = await fetchAllRawCommentsPages({ contentId });
- const comments = pages
- .map((page) => getCommentsFromRawData(page.comments))
- .flat()
- .sort(compareCommentsDate);
-
- return buildCommentsTree(comments);
-};
-
-/**
- * Send a comment using GraphQL API.
- *
- * @param {SendCommentVars} data - The comment data.
- * @returns {Promise<SentComment>} The mutation response.
- */
-export const sendComment = async (
- data: SendCommentInput
-): Promise<SentComment> => {
- const response = await fetchAPI<SentComment, typeof sendCommentMutation>({
- query: sendCommentMutation,
- variables: { ...data },
- });
-
- return response.createComment;
-};
diff --git a/src/services/graphql/contact.mutation.ts b/src/services/graphql/contact.mutation.ts
deleted file mode 100644
index b82fc07..0000000
--- a/src/services/graphql/contact.mutation.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * 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
deleted file mode 100644
index 3098374..0000000
--- a/src/services/graphql/contact.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { SendMailInput } from '../../types';
-import { fetchAPI } 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 {SendMailInput} data - The mail data.
- * @returns {Promise<SentEmail>} The mutation response.
- */
-export const sendMail = async (data: SendMailInput): Promise<SentEmail> => {
- const response = await fetchAPI<SentEmail, typeof sendMailMutation>({
- query: sendMailMutation,
- variables: { ...data },
- });
-
- return response.sendEmail;
-};
diff --git a/src/services/graphql/fetchers/comments/fetch-comments.ts b/src/services/graphql/fetchers/comments/fetch-comments.ts
new file mode 100644
index 0000000..85ae6c1
--- /dev/null
+++ b/src/services/graphql/fetchers/comments/fetch-comments.ts
@@ -0,0 +1,65 @@
+import type {
+ GraphQLCommentWhere,
+ GraphQLEdgesInput,
+ GraphQLNodes,
+ Nullable,
+ WPComment,
+} from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type CommentsListResponse = {
+ comments: Nullable<GraphQLNodes<WPComment>>;
+};
+
+const commentsListQuery = `query CommentsList($first: Int, $contentId: ID, $contentName: String, $status: String) {
+ comments(
+ first: $first
+ where: {contentId: $contentId, contentName: $contentName, order: ASC, orderby: COMMENT_DATE, status: $status}
+ ) {
+ nodes {
+ approved
+ author {
+ node {
+ avatar {
+ height
+ url
+ width
+ }
+ name
+ url
+ }
+ }
+ content
+ databaseId
+ date
+ parentDatabaseId
+ status
+ }
+ }
+}`;
+
+export type FetchCommentsListInput = Pick<GraphQLEdgesInput, 'first'> & {
+ where?: GraphQLCommentWhere;
+};
+
+/**
+ * Retrieve the comments list.
+ *
+ * @param {FetchCommentsListInput} input - The input to retrieve comments.
+ * @returns {Promise<WPComment[]>} An array of comments.
+ */
+export const fetchCommentsList = async ({
+ where,
+ ...vars
+}: FetchCommentsListInput): Promise<WPComment[]> => {
+ const response = await fetchGraphQL<CommentsListResponse>({
+ query: commentsListQuery,
+ url: getGraphQLUrl(),
+ variables: { ...vars, ...where },
+ });
+
+ if (!response.comments)
+ return Promise.reject(new Error('No comments found.'));
+
+ return response.comments.nodes;
+};
diff --git a/src/services/graphql/fetchers/comments/index.ts b/src/services/graphql/fetchers/comments/index.ts
new file mode 100644
index 0000000..6a15970
--- /dev/null
+++ b/src/services/graphql/fetchers/comments/index.ts
@@ -0,0 +1 @@
+export * from './fetch-comments';
diff --git a/src/services/graphql/fetchers/index.ts b/src/services/graphql/fetchers/index.ts
new file mode 100644
index 0000000..f45b1c0
--- /dev/null
+++ b/src/services/graphql/fetchers/index.ts
@@ -0,0 +1,4 @@
+export * from './comments';
+export * from './posts';
+export * from './thematics';
+export * from './topics';
diff --git a/src/services/graphql/fetchers/posts/fetch-all-posts-slugs.ts b/src/services/graphql/fetchers/posts/fetch-all-posts-slugs.ts
new file mode 100644
index 0000000..28f2bbf
--- /dev/null
+++ b/src/services/graphql/fetchers/posts/fetch-all-posts-slugs.ts
@@ -0,0 +1,34 @@
+import type { GraphQLNodes, Nullable, SlugNode } from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+import { fetchPostsCount } from './fetch-posts-count';
+
+type PostsSlugsResponse = {
+ posts: Nullable<GraphQLNodes<SlugNode>>;
+};
+
+const postsSlugsQuery = `query PostsSlugs($first: Int) {
+ posts(first: $first) {
+ nodes {
+ slug
+ }
+ }
+}`;
+
+/**
+ * Retrieve the WordPress posts slugs.
+ *
+ * @returns {Promise<string[]>} The posts slugs.
+ */
+export const fetchAllPostsSlugs = async (): Promise<string[]> => {
+ const postsCount = await fetchPostsCount();
+ const response = await fetchGraphQL<PostsSlugsResponse>({
+ query: postsSlugsQuery,
+ url: getGraphQLUrl(),
+ variables: { first: postsCount },
+ });
+
+ if (!response.posts)
+ return Promise.reject(new Error('Unable to find the posts slugs.'));
+
+ return response.posts.nodes.map((node) => node.slug);
+};
diff --git a/src/services/graphql/fetchers/posts/fetch-last-post-cursor.ts b/src/services/graphql/fetchers/posts/fetch-last-post-cursor.ts
new file mode 100644
index 0000000..d5ed174
--- /dev/null
+++ b/src/services/graphql/fetchers/posts/fetch-last-post-cursor.ts
@@ -0,0 +1,37 @@
+import type { GraphQLPageInfo, Nullable } from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type LastPostCursorResponse = {
+ posts: Nullable<{
+ pageInfo: Pick<GraphQLPageInfo, 'endCursor'>;
+ }>;
+};
+
+const lastPostCursorQuery = `query LastPostCursor($first: Int) {
+ posts(first: $first) {
+ pageInfo {
+ endCursor
+ }
+ }
+}`;
+
+/**
+ * Retrieve the cursor of the last post for a given number of posts.
+ *
+ * @param {number} count - The number of posts to fetch.
+ * @returns {Promise<string>} The cursor of the last post.
+ */
+export const fetchLastPostCursor = async (count: number): Promise<string> => {
+ const response = await fetchGraphQL<LastPostCursorResponse>({
+ url: getGraphQLUrl(),
+ query: lastPostCursorQuery,
+ variables: { first: count },
+ });
+
+ if (!response.posts?.pageInfo.endCursor)
+ return Promise.reject(
+ new Error('Unable to find the cursor of the last post.')
+ );
+
+ return response.posts.pageInfo.endCursor;
+};
diff --git a/src/services/graphql/fetchers/posts/fetch-post.ts b/src/services/graphql/fetchers/posts/fetch-post.ts
new file mode 100644
index 0000000..53c6bc3
--- /dev/null
+++ b/src/services/graphql/fetchers/posts/fetch-post.ts
@@ -0,0 +1,92 @@
+import type { Nullable, WPPost } from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type PostResponse = {
+ post: Nullable<WPPost>;
+};
+
+const postQuery = `query Post($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 {
+ name
+ }
+ }
+ commentCount
+ contentParts {
+ afterMore
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ wordsCount
+ }
+ modified
+ seo {
+ metaDesc
+ title
+ }
+ slug
+ title
+ }
+}`;
+
+/**
+ * Retrieve a WordPress post by slug.
+ *
+ * @param {string} slug - The post slug.
+ * @returns {Promise<WPPost>} The requested post.
+ */
+export const fetchPost = async (slug: string): Promise<WPPost> => {
+ const response = await fetchGraphQL<PostResponse>({
+ query: postQuery,
+ url: getGraphQLUrl(),
+ variables: { slug },
+ });
+
+ if (!response.post)
+ return Promise.reject(
+ new Error(`No post found for the following slug ${slug}.`)
+ );
+
+ return response.post;
+};
diff --git a/src/services/graphql/fetchers/posts/fetch-posts-count.ts b/src/services/graphql/fetchers/posts/fetch-posts-count.ts
new file mode 100644
index 0000000..a72af52
--- /dev/null
+++ b/src/services/graphql/fetchers/posts/fetch-posts-count.ts
@@ -0,0 +1,43 @@
+import type {
+ GraphQLPageInfo,
+ GraphQLPostWhere,
+ Nullable,
+} from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type PostsCountResponse = {
+ posts: Nullable<{
+ pageInfo: Pick<GraphQLPageInfo, 'total'>;
+ }>;
+};
+
+const postsCountQuery = `query PostsCount($authorName: String, $search: String, $title: String) {
+ posts(where: {authorName: $authorName, search: $search, title: $title}) {
+ pageInfo {
+ total
+ }
+ }
+}`;
+
+/**
+ * Retrieve the total of WordPress posts.
+ *
+ * @param {GraphQLPostWhere} [input] - The input to filter the posts.
+ * @returns {Promise<number>} The total number of posts.
+ */
+export const fetchPostsCount = async (
+ input?: GraphQLPostWhere
+): Promise<number> => {
+ const response = await fetchGraphQL<PostsCountResponse>({
+ query: postsCountQuery,
+ url: getGraphQLUrl(),
+ variables: { ...input },
+ });
+
+ if (!response.posts)
+ return Promise.reject(
+ new Error('Unable to find the total number of posts.')
+ );
+
+ return response.posts.pageInfo.total;
+};
diff --git a/src/services/graphql/fetchers/posts/fetch-posts-list.ts b/src/services/graphql/fetchers/posts/fetch-posts-list.ts
new file mode 100644
index 0000000..452892b
--- /dev/null
+++ b/src/services/graphql/fetchers/posts/fetch-posts-list.ts
@@ -0,0 +1,97 @@
+import type {
+ GraphQLConnection,
+ GraphQLEdgesInput,
+ GraphQLPostOrderBy,
+ GraphQLPostWhere,
+ Nullable,
+ WPPostPreview,
+} from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type PostsListResponse = {
+ posts: Nullable<GraphQLConnection<WPPostPreview>>;
+};
+
+const postsListQuery = `query PostsList($after: String, $before: String, $first: Int, $last: Int, $authorName: String, $orderby: [PostObjectsConnectionOrderbyInput], $search: String, $title: String) {
+ posts(
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ where: {authorName: $authorName, orderby: $orderby, search: $search, title: $title}
+ ) {
+ 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
+ }
+ }
+}`;
+
+export type FetchPostsListInput = GraphQLEdgesInput & {
+ orderBy?: GraphQLPostOrderBy;
+ where?: GraphQLPostWhere;
+};
+
+/**
+ * Retrieve a paginated list of WordPress posts.
+ *
+ * @param {FetchPostsListInput} input - The input to retrieve posts.
+ * @returns {Promise<GraphQLConnection<WPPostPreview>>} The paginated posts.
+ */
+export const fetchPostsList = async ({
+ orderBy,
+ where,
+ ...vars
+}: FetchPostsListInput): Promise<GraphQLConnection<WPPostPreview>> => {
+ const response = await fetchGraphQL<PostsListResponse>({
+ query: postsListQuery,
+ url: getGraphQLUrl(),
+ variables: {
+ ...vars,
+ ...where,
+ orderBy: orderBy ? [orderBy] : undefined,
+ },
+ });
+
+ if (!response.posts) return Promise.reject(new Error('No posts found.'));
+
+ return response.posts;
+};
diff --git a/src/services/graphql/fetchers/posts/fetch-recent-posts.ts b/src/services/graphql/fetchers/posts/fetch-recent-posts.ts
new file mode 100644
index 0000000..12785d6
--- /dev/null
+++ b/src/services/graphql/fetchers/posts/fetch-recent-posts.ts
@@ -0,0 +1,76 @@
+import type {
+ GraphQLConnection,
+ GraphQLEdgesInput,
+ GraphQLPostWhere,
+ Nullable,
+ RecentWPPost,
+} from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type RecentPostsResponse = {
+ posts: Nullable<GraphQLConnection<RecentWPPost>>;
+};
+
+const recentPostsQuery = `query RecentPosts($after: String, $before: String, $first: Int, $last: Int, $authorName: String, $search: String, $title: String) {
+ posts(
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ where: {authorName: $authorName, search: $search, title: $title, orderby: {field: DATE, order: DESC}}
+ ) {
+ edges {
+ cursor
+ node {
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ slug
+ title
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ total
+ }
+ }
+}`;
+
+export type FetchRecentPostsInput = GraphQLEdgesInput & {
+ where?: GraphQLPostWhere;
+};
+
+/**
+ * Retrieve a paginated list of recent WordPress posts.
+ *
+ * @param {FetchRecentPostsInput} input - The input to retrieve recent posts.
+ * @returns {Promise<GraphQLConnection<RecentWPPost>>} The recent posts.
+ */
+export const fetchRecentPosts = async ({
+ where,
+ ...vars
+}: FetchRecentPostsInput): Promise<GraphQLConnection<RecentWPPost>> => {
+ const response = await fetchGraphQL<RecentPostsResponse>({
+ query: recentPostsQuery,
+ url: getGraphQLUrl(),
+ variables: { ...vars, ...where },
+ });
+
+ if (!response.posts)
+ return Promise.reject(new Error('No recent posts found.'));
+
+ return response.posts;
+};
diff --git a/src/services/graphql/fetchers/posts/index.ts b/src/services/graphql/fetchers/posts/index.ts
new file mode 100644
index 0000000..fd725cd
--- /dev/null
+++ b/src/services/graphql/fetchers/posts/index.ts
@@ -0,0 +1,6 @@
+export * from './fetch-all-posts-slugs';
+export * from './fetch-last-post-cursor';
+export * from './fetch-post';
+export * from './fetch-posts-count';
+export * from './fetch-posts-list';
+export * from './fetch-recent-posts';
diff --git a/src/services/graphql/fetchers/thematics/fetch-all-thematics-slugs.ts b/src/services/graphql/fetchers/thematics/fetch-all-thematics-slugs.ts
new file mode 100644
index 0000000..739c009
--- /dev/null
+++ b/src/services/graphql/fetchers/thematics/fetch-all-thematics-slugs.ts
@@ -0,0 +1,34 @@
+import type { GraphQLNodes, Nullable, SlugNode } from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+import { fetchThematicsCount } from './fetch-thematics-count';
+
+type ThematicsSlugsResponse = {
+ thematics: Nullable<GraphQLNodes<SlugNode>>;
+};
+
+const thematicsSlugsQuery = `query ThematicsSlugs($first: Int) {
+ thematics(first: $first) {
+ nodes {
+ slug
+ }
+ }
+}`;
+
+/**
+ * Retrieve the WordPress thematics slugs.
+ *
+ * @returns {Promise<string[]>} The thematics slugs.
+ */
+export const fetchAllThematicsSlugs = async (): Promise<string[]> => {
+ const thematicsCount = await fetchThematicsCount();
+ const response = await fetchGraphQL<ThematicsSlugsResponse>({
+ query: thematicsSlugsQuery,
+ url: getGraphQLUrl(),
+ variables: { first: thematicsCount },
+ });
+
+ if (!response.thematics)
+ return Promise.reject(new Error('Unable to find the thematics slugs.'));
+
+ return response.thematics.nodes.map((node) => node.slug);
+};
diff --git a/src/services/graphql/fetchers/thematics/fetch-thematic.ts b/src/services/graphql/fetchers/thematics/fetch-thematic.ts
new file mode 100644
index 0000000..a9958bc
--- /dev/null
+++ b/src/services/graphql/fetchers/thematics/fetch-thematic.ts
@@ -0,0 +1,96 @@
+import type { Nullable, WPThematic } from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type ThematicResponse = {
+ thematic: Nullable<WPThematic>;
+};
+
+const thematicQuery = `query Thematic($slug: ID!) {
+ thematic(id: $slug, idType: SLUG) {
+ acfThematics {
+ postsInThematic {
+ ... on Post {
+ acfPosts {
+ postsInTopic {
+ ... on Topic {
+ databaseId
+ slug
+ title
+ }
+ }
+ }
+ author {
+ node {
+ name
+ }
+ }
+ commentCount
+ contentParts {
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ wordsCount
+ }
+ modified
+ slug
+ title
+ }
+ }
+ }
+ contentParts {
+ afterMore
+ beforeMore
+ }
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ seo {
+ metaDesc
+ title
+ }
+ slug
+ title
+ }
+}`;
+
+/**
+ * Retrieve a WordPress thematic by slug.
+ *
+ * @param {string} slug - The thematic slug.
+ * @returns {Promise<WPThematic>} The requested thematic.
+ */
+export const fetchThematic = async (slug: string): Promise<WPThematic> => {
+ const response = await fetchGraphQL<ThematicResponse>({
+ query: thematicQuery,
+ url: getGraphQLUrl(),
+ variables: { slug },
+ });
+
+ if (!response.thematic)
+ return Promise.reject(
+ new Error(`No thematic found for the following slug ${slug}.`)
+ );
+
+ return response.thematic;
+};
diff --git a/src/services/graphql/fetchers/thematics/fetch-thematics-count.ts b/src/services/graphql/fetchers/thematics/fetch-thematics-count.ts
new file mode 100644
index 0000000..29a3b17
--- /dev/null
+++ b/src/services/graphql/fetchers/thematics/fetch-thematics-count.ts
@@ -0,0 +1,43 @@
+import type {
+ GraphQLPageInfo,
+ GraphQLTaxonomyWhere,
+ Nullable,
+} from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type ThematicsCountResponse = {
+ thematics: Nullable<{
+ pageInfo: Pick<GraphQLPageInfo, 'total'>;
+ }>;
+};
+
+const thematicsCountQuery = `query ThematicsCount($search: String, $title: String) {
+ thematics(where: {search: $search, title: $title}) {
+ pageInfo {
+ total
+ }
+ }
+}`;
+
+/**
+ * Retrieve the total of WordPress thematics.
+ *
+ * @param {GraphQLTaxonomyWhere} [input] - The input to filter the thematics.
+ * @returns {Promise<number>} The total number of thematics.
+ */
+export const fetchThematicsCount = async (
+ input?: GraphQLTaxonomyWhere
+): Promise<number> => {
+ const response = await fetchGraphQL<ThematicsCountResponse>({
+ query: thematicsCountQuery,
+ url: getGraphQLUrl(),
+ variables: { ...input },
+ });
+
+ if (!response.thematics)
+ return Promise.reject(
+ new Error('Unable to find the total number of thematics.')
+ );
+
+ return response.thematics.pageInfo.total;
+};
diff --git a/src/services/graphql/fetchers/thematics/fetch-thematics-list.ts b/src/services/graphql/fetchers/thematics/fetch-thematics-list.ts
new file mode 100644
index 0000000..f4d22c6
--- /dev/null
+++ b/src/services/graphql/fetchers/thematics/fetch-thematics-list.ts
@@ -0,0 +1,78 @@
+import type {
+ GraphQLConnection,
+ GraphQLEdgesInput,
+ GraphQLTaxonomyOrderBy,
+ GraphQLTaxonomyWhere,
+ Nullable,
+ WPThematicPreview,
+} from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type ThematicsListResponse = {
+ thematics: Nullable<GraphQLConnection<WPThematicPreview>>;
+};
+
+const thematicsListQuery = `query ThematicsList($after: String, $before: String, $first: Int, $last: Int, $orderby: [PostObjectsConnectionOrderbyInput], $search: String, $title: String) {
+ thematics(
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ where: {orderby: $orderby, search: $search, title: $title}
+ ) {
+ edges {
+ cursor
+ node {
+ contentParts {
+ beforeMore
+ }
+ databaseId
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ slug
+ title
+ }
+ }
+ }
+}`;
+
+export type FetchThematicsListInput = GraphQLEdgesInput & {
+ orderBy?: GraphQLTaxonomyOrderBy;
+ where?: GraphQLTaxonomyWhere;
+};
+
+/**
+ * Retrieve a paginated list of WordPress thematics.
+ *
+ * @param {FetchThematicsListInput} input - The input to retrieve thematics.
+ * @returns {Promise<GraphQLConnection<WPThematicPreview>>} The paginated thematics.
+ */
+export const fetchThematicsList = async ({
+ orderBy,
+ where,
+ ...vars
+}: FetchThematicsListInput): Promise<GraphQLConnection<WPThematicPreview>> => {
+ const response = await fetchGraphQL<ThematicsListResponse>({
+ query: thematicsListQuery,
+ url: getGraphQLUrl(),
+ variables: {
+ ...vars,
+ ...where,
+ orderBy: orderBy ? [orderBy] : undefined,
+ },
+ });
+
+ if (!response.thematics)
+ return Promise.reject(new Error('No thematics found.'));
+
+ return response.thematics;
+};
diff --git a/src/services/graphql/fetchers/thematics/index.ts b/src/services/graphql/fetchers/thematics/index.ts
new file mode 100644
index 0000000..c002793
--- /dev/null
+++ b/src/services/graphql/fetchers/thematics/index.ts
@@ -0,0 +1,4 @@
+export * from './fetch-all-thematics-slugs';
+export * from './fetch-thematic';
+export * from './fetch-thematics-count';
+export * from './fetch-thematics-list';
diff --git a/src/services/graphql/fetchers/topics/fetch-all-topics-slugs.ts b/src/services/graphql/fetchers/topics/fetch-all-topics-slugs.ts
new file mode 100644
index 0000000..eab4a7c
--- /dev/null
+++ b/src/services/graphql/fetchers/topics/fetch-all-topics-slugs.ts
@@ -0,0 +1,34 @@
+import type { GraphQLNodes, Nullable, SlugNode } from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+import { fetchTopicsCount } from './fetch-topics-count';
+
+type TopicsSlugsResponse = {
+ topics: Nullable<GraphQLNodes<SlugNode>>;
+};
+
+const topicsSlugsQuery = `query TopicsSlugs($first: Int) {
+ topics(first: $first) {
+ nodes {
+ slug
+ }
+ }
+}`;
+
+/**
+ * Retrieve the WordPress topics slugs.
+ *
+ * @returns {Promise<string[]>} The topics slugs.
+ */
+export const fetchAllTopicsSlugs = async (): Promise<string[]> => {
+ const topicsCount = await fetchTopicsCount();
+ const response = await fetchGraphQL<TopicsSlugsResponse>({
+ query: topicsSlugsQuery,
+ url: getGraphQLUrl(),
+ variables: { first: topicsCount },
+ });
+
+ if (!response.topics)
+ return Promise.reject(new Error('Unable to find the topics slugs.'));
+
+ return response.topics.nodes.map((node) => node.slug);
+};
diff --git a/src/services/graphql/fetchers/topics/fetch-topic.ts b/src/services/graphql/fetchers/topics/fetch-topic.ts
new file mode 100644
index 0000000..efc1d9e
--- /dev/null
+++ b/src/services/graphql/fetchers/topics/fetch-topic.ts
@@ -0,0 +1,97 @@
+import type { Nullable, WPTopic } from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type TopicResponse = {
+ topic: Nullable<WPTopic>;
+};
+
+const topicQuery = `query Topic($slug: ID!) {
+ topic(id: $slug, idType: SLUG) {
+ acfTopics {
+ officialWebsite
+ postsInTopic {
+ ... on Post {
+ acfPosts {
+ postsInThematic {
+ ... on Thematic {
+ databaseId
+ slug
+ title
+ }
+ }
+ }
+ author {
+ node {
+ name
+ }
+ }
+ commentCount
+ contentParts {
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ wordsCount
+ }
+ modified
+ slug
+ title
+ }
+ }
+ }
+ contentParts {
+ afterMore
+ beforeMore
+ }
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ seo {
+ metaDesc
+ title
+ }
+ slug
+ title
+ }
+}`;
+
+/**
+ * Retrieve a WordPress topic by slug.
+ *
+ * @param {string} slug - The topic slug.
+ * @returns {Promise<WPTopic>} The requested topic.
+ */
+export const fetchTopic = async (slug: string): Promise<WPTopic> => {
+ const response = await fetchGraphQL<TopicResponse>({
+ query: topicQuery,
+ url: getGraphQLUrl(),
+ variables: { slug },
+ });
+
+ if (!response.topic)
+ return Promise.reject(
+ new Error(`No topic found for the following slug ${slug}.`)
+ );
+
+ return response.topic;
+};
diff --git a/src/services/graphql/fetchers/topics/fetch-topics-count.ts b/src/services/graphql/fetchers/topics/fetch-topics-count.ts
new file mode 100644
index 0000000..868b01e
--- /dev/null
+++ b/src/services/graphql/fetchers/topics/fetch-topics-count.ts
@@ -0,0 +1,43 @@
+import type {
+ GraphQLPageInfo,
+ GraphQLTaxonomyWhere,
+ Nullable,
+} from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type TopicsCountResponse = {
+ topics: Nullable<{
+ pageInfo: Pick<GraphQLPageInfo, 'total'>;
+ }>;
+};
+
+const topicsCountQuery = `query TopicsCount($search: String, $title: String) {
+ topics(where: {search: $search, title: $title}) {
+ pageInfo {
+ total
+ }
+ }
+}`;
+
+/**
+ * Retrieve the total of WordPress topics.
+ *
+ * @param {GraphQLTaxonomyWhere} [input] - The input to filter the topics.
+ * @returns {Promise<number>} The total number of topics.
+ */
+export const fetchTopicsCount = async (
+ input?: GraphQLTaxonomyWhere
+): Promise<number> => {
+ const response = await fetchGraphQL<TopicsCountResponse>({
+ query: topicsCountQuery,
+ url: getGraphQLUrl(),
+ variables: { ...input },
+ });
+
+ if (!response.topics)
+ return Promise.reject(
+ new Error('Unable to find the total number of topics.')
+ );
+
+ return response.topics.pageInfo.total;
+};
diff --git a/src/services/graphql/fetchers/topics/fetch-topics-list.ts b/src/services/graphql/fetchers/topics/fetch-topics-list.ts
new file mode 100644
index 0000000..1bc2e38
--- /dev/null
+++ b/src/services/graphql/fetchers/topics/fetch-topics-list.ts
@@ -0,0 +1,84 @@
+import type {
+ GraphQLConnection,
+ GraphQLEdgesInput,
+ GraphQLTaxonomyOrderBy,
+ GraphQLTaxonomyWhere,
+ Nullable,
+ WPTopicPreview,
+} from '../../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../../utils/helpers';
+
+type TopicsListResponse = {
+ topics: Nullable<GraphQLConnection<WPTopicPreview>>;
+};
+
+const topicsListQuery = `query TopicsList($after: String, $before: String, $first: Int, $last: Int, $orderby: [PostObjectsConnectionOrderbyInput], $search: String, $title: String) {
+ topics(
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ where: {orderby: $orderby, search: $search, title: $title}
+ ) {
+ edges {
+ cursor
+ node {
+ contentParts {
+ beforeMore
+ }
+ databaseId
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ slug
+ title
+ }
+ }
+ slug
+ title
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ total
+ }
+ }
+}`;
+
+export type FetchTopicsListInput = GraphQLEdgesInput & {
+ orderBy?: GraphQLTaxonomyOrderBy;
+ where?: GraphQLTaxonomyWhere;
+};
+
+/**
+ * Retrieve a paginated list of WordPress topics.
+ *
+ * @param {FetchTopicsListInput} input - The input to retrieve topics.
+ * @returns {Promise<GraphQLConnection<WPTopicPreview>>} The paginated topics.
+ */
+export const fetchTopicsList = async ({
+ orderBy,
+ where,
+ ...vars
+}: FetchTopicsListInput): Promise<GraphQLConnection<WPTopicPreview>> => {
+ const response = await fetchGraphQL<TopicsListResponse>({
+ query: topicsListQuery,
+ url: getGraphQLUrl(),
+ variables: {
+ ...vars,
+ ...where,
+ orderBy: orderBy ? [orderBy] : undefined,
+ },
+ });
+
+ if (!response.topics) return Promise.reject(new Error('No topics found.'));
+
+ return response.topics;
+};
diff --git a/src/services/graphql/fetchers/topics/index.ts b/src/services/graphql/fetchers/topics/index.ts
new file mode 100644
index 0000000..e381883
--- /dev/null
+++ b/src/services/graphql/fetchers/topics/index.ts
@@ -0,0 +1,4 @@
+export * from './fetch-all-topics-slugs';
+export * from './fetch-topic';
+export * from './fetch-topics-count';
+export * from './fetch-topics-list';
diff --git a/src/services/graphql/helpers/build-comments-tree.test.ts b/src/services/graphql/helpers/build-comments-tree.test.ts
new file mode 100644
index 0000000..cd9fa40
--- /dev/null
+++ b/src/services/graphql/helpers/build-comments-tree.test.ts
@@ -0,0 +1,67 @@
+import { describe, expect, it } from '@jest/globals';
+import type { SingleComment } from '../../../types';
+import { buildCommentsTree } from './build-comments-tree';
+
+describe('build-comments-tree', () => {
+ it('transforms a flat comments array to a comments tree', () => {
+ const firstComment = {
+ content: 'Non non provident mollitia a.',
+ id: 1,
+ isApproved: true,
+ meta: { author: { name: 'Emma_Muller' }, date: '2022-11-02' },
+ replies: [],
+ } satisfies SingleComment;
+ const firstCommentReplies = [
+ {
+ content: 'Et omnis voluptatem est atque.',
+ id: 3,
+ isApproved: true,
+ meta: { author: { name: 'Patrick.Goodwin44' }, date: '2022-11-05' },
+ replies: [],
+ parentId: 1,
+ },
+ ] satisfies SingleComment[];
+ const secondComment = {
+ content: 'Vero iure architecto modi iusto qui.',
+ id: 2,
+ isApproved: true,
+ meta: { author: { name: 'Dominique13' }, date: '2022-11-04' },
+ replies: [],
+ } satisfies SingleComment;
+ const secondCommentReplies = [
+ {
+ content: 'Qui quaerat quas quia praesentium quasi.',
+ id: 4,
+ isApproved: true,
+ meta: { author: { name: 'Patrick.Goodwin44' }, date: '2022-11-05' },
+ replies: [],
+ parentId: 2,
+ },
+ {
+ content: 'Ut officia aliquid harum voluptas molestiae quo.',
+ id: 5,
+ isApproved: true,
+ meta: { author: { name: 'Ariel.Braun6' }, date: '2022-11-06' },
+ replies: [],
+ parentId: 2,
+ },
+ ] satisfies SingleComment[];
+ const comments: SingleComment[] = [
+ firstComment,
+ secondComment,
+ ...firstCommentReplies,
+ ...secondCommentReplies,
+ ];
+ const result = buildCommentsTree(comments);
+
+ expect(result).toHaveLength(2);
+ expect(result[0]).toStrictEqual({
+ ...firstComment,
+ replies: firstCommentReplies,
+ });
+ expect(result[1]).toStrictEqual({
+ ...secondComment,
+ replies: secondCommentReplies,
+ });
+ });
+});
diff --git a/src/services/graphql/helpers/build-comments-tree.ts b/src/services/graphql/helpers/build-comments-tree.ts
new file mode 100644
index 0000000..1534cfe
--- /dev/null
+++ b/src/services/graphql/helpers/build-comments-tree.ts
@@ -0,0 +1,30 @@
+import type { SingleComment } from '../../../types';
+
+/**
+ * Create a comments tree with replies.
+ *
+ * @param {SingleComment[]} comments - A flatten comments list.
+ * @returns {SingleComment[]} An array of comments with replies.
+ */
+export const buildCommentsTree = (
+ comments: SingleComment[]
+): SingleComment[] => {
+ type CommentsHashTable = Record<string, SingleComment>;
+
+ const hashTable: CommentsHashTable = Object.create(null);
+ const commentsTree: SingleComment[] = [];
+
+ comments.forEach((comment) => {
+ hashTable[comment.id] = { ...comment, replies: [] };
+ });
+
+ comments.forEach((comment) => {
+ if (comment.parentId) {
+ hashTable[comment.parentId].replies.push(hashTable[comment.id]);
+ } else {
+ commentsTree.push(hashTable[comment.id]);
+ }
+ });
+
+ return commentsTree;
+};
diff --git a/src/services/graphql/helpers/convert-post-preview-to-article-preview.test.ts b/src/services/graphql/helpers/convert-post-preview-to-article-preview.test.ts
new file mode 100644
index 0000000..c13684f
--- /dev/null
+++ b/src/services/graphql/helpers/convert-post-preview-to-article-preview.test.ts
@@ -0,0 +1,130 @@
+import { describe, expect, it } from '@jest/globals';
+import type { WPPostPreview, WPThematicPreview } from '../../../types';
+import { convertPostPreviewToArticlePreview } from './convert-post-preview-to-article-preview';
+import { convertTaxonomyToPageLink } from './convert-taxonomy-to-page-link';
+import { convertWPImgToImg } from './convert-wp-image-to-img';
+
+describe('convert-post-preview-to-article-preview', () => {
+ /* eslint-disable max-statements */
+ it('converts a RecentWPPost object to a RecentArticle object', () => {
+ const post: WPPostPreview = {
+ acfPosts: null,
+ commentCount: 6,
+ contentParts: {
+ beforeMore:
+ 'Et quos fuga molestias. Voluptatum nobis mollitia eaque dolorem sunt. Dolores dignissimos consequuntur mollitia. Enim molestias quibusdam sequi. Dolore ut quo. Libero iure non vel reprehenderit.',
+ },
+ databaseId: 5,
+ date: '2021-04-28',
+ featuredImage: null,
+ info: {
+ wordsCount: 450,
+ },
+ modified: '2021-04-29',
+ slug: '/the-post-slug',
+ title: 'et tempore sint',
+ };
+ const result = convertPostPreviewToArticlePreview(post);
+
+ expect(result.id).toBe(post.databaseId);
+ expect(result.intro).toBe(post.contentParts.beforeMore);
+ expect(result.meta.commentsCount).toBe(post.commentCount);
+ expect(result.meta.cover).toBeUndefined();
+ expect(result.meta.dates.publication).toBe(post.date);
+ expect(result.meta.dates.update).toBe(post.modified);
+ expect(result.meta.thematics).toBeUndefined();
+ expect(result.meta.wordsCount).toBe(post.info.wordsCount);
+ expect(result.slug).toBe(post.slug);
+ expect(result.title).toBe(post.title);
+ });
+ /* eslint-enable max-statements */
+
+ it('can return 0 as comment count if not defined', () => {
+ const post: WPPostPreview = {
+ acfPosts: null,
+ commentCount: null,
+ contentParts: {
+ beforeMore:
+ 'Et quos fuga molestias. Voluptatum nobis mollitia eaque dolorem sunt. Dolores dignissimos consequuntur mollitia. Enim molestias quibusdam sequi. Dolore ut quo. Libero iure non vel reprehenderit.',
+ },
+ databaseId: 5,
+ date: '2021-04-28',
+ featuredImage: null,
+ info: {
+ wordsCount: 450,
+ },
+ modified: '2021-04-29',
+ slug: '/the-post-slug',
+ title: 'et tempore sint',
+ };
+ const result = convertPostPreviewToArticlePreview(post);
+
+ expect(result.meta.commentsCount).toBe(0);
+ });
+
+ it('can convert the cover', () => {
+ const post = {
+ acfPosts: null,
+ commentCount: null,
+ contentParts: {
+ beforeMore:
+ 'Et quos fuga molestias. Voluptatum nobis mollitia eaque dolorem sunt. Dolores dignissimos consequuntur mollitia. Enim molestias quibusdam sequi. Dolore ut quo. Libero iure non vel reprehenderit.',
+ },
+ databaseId: 5,
+ date: '2021-04-28',
+ featuredImage: {
+ node: {
+ altText: 'molestiae praesentium animi',
+ mediaDetails: {
+ height: 480,
+ width: 640,
+ },
+ sourceUrl: 'https://picsum.photos/640/480',
+ title: 'ullam deserunt perspiciatis',
+ },
+ },
+ info: {
+ wordsCount: 450,
+ },
+ modified: '2021-04-29',
+ slug: '/the-post-slug',
+ title: 'et tempore sint',
+ } satisfies WPPostPreview;
+ const result = convertPostPreviewToArticlePreview(post);
+
+ expect(result.meta.cover).toStrictEqual(
+ convertWPImgToImg(post.featuredImage.node)
+ );
+ });
+
+ it('can convert the thematics', () => {
+ const thematics: WPThematicPreview[] = [
+ { databaseId: 2, slug: '/thematic1', title: 'aut quis vel' },
+ { databaseId: 8, slug: '/thematic2', title: 'vel sint autem' },
+ ];
+ const post: WPPostPreview = {
+ acfPosts: {
+ postsInThematic: thematics,
+ },
+ commentCount: 6,
+ contentParts: {
+ beforeMore:
+ 'Et quos fuga molestias. Voluptatum nobis mollitia eaque dolorem sunt. Dolores dignissimos consequuntur mollitia. Enim molestias quibusdam sequi. Dolore ut quo. Libero iure non vel reprehenderit.',
+ },
+ databaseId: 5,
+ date: '2021-04-28',
+ featuredImage: null,
+ info: {
+ wordsCount: 450,
+ },
+ modified: '2021-04-29',
+ slug: '/the-post-slug',
+ title: 'et tempore sint',
+ };
+ const result = convertPostPreviewToArticlePreview(post);
+
+ expect(result.meta.thematics).toStrictEqual(
+ thematics.map(convertTaxonomyToPageLink)
+ );
+ });
+});
diff --git a/src/services/graphql/helpers/convert-post-preview-to-article-preview.ts b/src/services/graphql/helpers/convert-post-preview-to-article-preview.ts
new file mode 100644
index 0000000..78777eb
--- /dev/null
+++ b/src/services/graphql/helpers/convert-post-preview-to-article-preview.ts
@@ -0,0 +1,36 @@
+import type { ArticlePreview, WPPostPreview } from '../../../types';
+import { convertTaxonomyToPageLink } from './convert-taxonomy-to-page-link';
+import { convertWPImgToImg } from './convert-wp-image-to-img';
+
+export const convertPostPreviewToArticlePreview = ({
+ acfPosts,
+ commentCount,
+ contentParts,
+ databaseId,
+ date,
+ featuredImage,
+ info,
+ modified,
+ slug,
+ title,
+}: WPPostPreview): ArticlePreview => {
+ return {
+ id: databaseId,
+ intro: contentParts.beforeMore,
+ meta: {
+ commentsCount: typeof commentCount === 'number' ? commentCount : 0,
+ cover: featuredImage ? convertWPImgToImg(featuredImage.node) : undefined,
+ dates: {
+ publication: date,
+ update: modified,
+ },
+ thematics:
+ acfPosts && 'postsInThematic' in acfPosts
+ ? acfPosts.postsInThematic?.map(convertTaxonomyToPageLink)
+ : undefined,
+ wordsCount: info.wordsCount,
+ },
+ slug,
+ title,
+ };
+};
diff --git a/src/services/graphql/helpers/convert-post-to-article.test.ts b/src/services/graphql/helpers/convert-post-to-article.test.ts
new file mode 100644
index 0000000..0a1c359
--- /dev/null
+++ b/src/services/graphql/helpers/convert-post-to-article.test.ts
@@ -0,0 +1,125 @@
+import { describe, expect, it } from '@jest/globals';
+import type { WPPost } from '../../../types';
+import { convertPostToArticle } from './convert-post-to-article';
+import { convertWPImgToImg } from './convert-wp-image-to-img';
+
+describe('convert-post-to-article', () => {
+ /* eslint-disable max-statements */
+ it('converts a WPPost object to an Article object', async () => {
+ const post: WPPost = {
+ acfPosts: null,
+ author: { node: { name: 'Vince5' } },
+ commentCount: 10,
+ contentParts: {
+ afterMore:
+ 'Eum est rerum neque placeat iure veniam enim consequatur assumenda. Quos eos placeat ea et vel sit ratione fugit. Modi qui sint iure beatae illo voluptas.',
+ beforeMore:
+ 'Omnis ab qui dolorem praesentium voluptas asperiores officiis. Id nostrum minus quae ducimus tenetur eum a rem eum. Aut odio libero sit soluta ullam odit.',
+ },
+ databaseId: 8,
+ date: '2022-05-04',
+ featuredImage: null,
+ info: { wordsCount: 300 },
+ modified: '2022-06-01',
+ seo: {
+ metaDesc: 'Est non debitis quas harum quasi voluptatem qui.',
+ title: 'consequuntur molestiae amet',
+ },
+ slug: '/the-post-slug',
+ title: 'ea vero repellat',
+ };
+ const result = await convertPostToArticle(post);
+
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
+ expect.assertions(15);
+
+ expect(result.content).toBe(post.contentParts.afterMore);
+ expect(result.id).toBe(post.databaseId);
+ expect(result.intro).toBe(post.contentParts.beforeMore);
+ expect(result.meta.author).toBe(post.author.node.name);
+ expect(result.meta.commentsCount).toBe(post.commentCount);
+ expect(result.meta.cover).toBeUndefined();
+ expect(result.meta.dates.publication).toBe(post.date);
+ expect(result.meta.dates.update).toBe(post.modified);
+ expect(result.meta.seo.description).toBe(post.seo.metaDesc);
+ expect(result.meta.seo.title).toBe(post.seo.title);
+ expect(result.meta.thematics).toBeUndefined();
+ expect(result.meta.topics).toBeUndefined();
+ expect(result.meta.wordsCount).toBe(post.info.wordsCount);
+ expect(result.slug).toBe(post.slug);
+ expect(result.title).toBe(post.title);
+ });
+ /* eslint-enable max-statements */
+
+ it('can convert the cover', async () => {
+ const post = {
+ acfPosts: null,
+ author: { node: { name: 'Vince5' } },
+ commentCount: null,
+ contentParts: {
+ afterMore:
+ 'Eum est rerum neque placeat iure veniam enim consequatur assumenda. Quos eos placeat ea et vel sit ratione fugit. Modi qui sint iure beatae illo voluptas.',
+ beforeMore:
+ 'Omnis ab qui dolorem praesentium voluptas asperiores officiis. Id nostrum minus quae ducimus tenetur eum a rem eum. Aut odio libero sit soluta ullam odit.',
+ },
+ databaseId: 8,
+ date: '2022-05-04',
+ featuredImage: {
+ node: {
+ altText: 'molestiae praesentium animi',
+ mediaDetails: {
+ height: 480,
+ width: 640,
+ },
+ sourceUrl: 'https://picsum.photos/640/480',
+ title: 'ullam deserunt perspiciatis',
+ },
+ },
+ info: { wordsCount: 300 },
+ modified: '2022-06-01',
+ seo: {
+ metaDesc: 'Est non debitis quas harum quasi voluptatem qui.',
+ title: 'consequuntur molestiae amet',
+ },
+ slug: '/the-post-slug',
+ title: 'ea vero repellat',
+ } satisfies WPPost;
+ const result = await convertPostToArticle(post);
+
+ expect.assertions(1);
+
+ expect(result.meta.cover).toStrictEqual(
+ convertWPImgToImg(post.featuredImage.node)
+ );
+ });
+
+ it('can return 0 as comment count when not defined', async () => {
+ const post: WPPost = {
+ acfPosts: null,
+ author: { node: { name: 'Vince5' } },
+ commentCount: null,
+ contentParts: {
+ afterMore:
+ 'Eum est rerum neque placeat iure veniam enim consequatur assumenda. Quos eos placeat ea et vel sit ratione fugit. Modi qui sint iure beatae illo voluptas.',
+ beforeMore:
+ 'Omnis ab qui dolorem praesentium voluptas asperiores officiis. Id nostrum minus quae ducimus tenetur eum a rem eum. Aut odio libero sit soluta ullam odit.',
+ },
+ databaseId: 8,
+ date: '2022-05-04',
+ featuredImage: null,
+ info: { wordsCount: 300 },
+ modified: '2022-06-01',
+ seo: {
+ metaDesc: 'Est non debitis quas harum quasi voluptatem qui.',
+ title: 'consequuntur molestiae amet',
+ },
+ slug: '/the-post-slug',
+ title: 'ea vero repellat',
+ };
+ const result = await convertPostToArticle(post);
+
+ expect.assertions(1);
+
+ expect(result.meta.commentsCount).toBe(0);
+ });
+});
diff --git a/src/services/graphql/helpers/convert-post-to-article.ts b/src/services/graphql/helpers/convert-post-to-article.ts
new file mode 100644
index 0000000..b540a77
--- /dev/null
+++ b/src/services/graphql/helpers/convert-post-to-article.ts
@@ -0,0 +1,43 @@
+import type { Article, WPPost } from '../../../types';
+import { updateContentTree } from '../../../utils/helpers';
+import { convertTaxonomyToPageLink } from './convert-taxonomy-to-page-link';
+import { convertWPImgToImg } from './convert-wp-image-to-img';
+
+export const convertPostToArticle = async ({
+ acfPosts,
+ author,
+ commentCount,
+ contentParts,
+ databaseId,
+ date,
+ featuredImage,
+ info,
+ modified,
+ seo,
+ slug,
+ title,
+}: WPPost): Promise<Article> => {
+ return {
+ content: await updateContentTree(contentParts.afterMore),
+ id: databaseId,
+ intro: contentParts.beforeMore,
+ meta: {
+ author: author.node.name,
+ commentsCount: commentCount ?? 0,
+ cover: featuredImage ? convertWPImgToImg(featuredImage.node) : undefined,
+ dates: {
+ publication: date,
+ update: modified,
+ },
+ seo: {
+ description: seo.metaDesc,
+ title: seo.title,
+ },
+ thematics: acfPosts?.postsInThematic?.map(convertTaxonomyToPageLink),
+ topics: acfPosts?.postsInTopic?.map(convertTaxonomyToPageLink),
+ wordsCount: info.wordsCount,
+ },
+ slug,
+ title,
+ };
+};
diff --git a/src/services/graphql/helpers/convert-recent-post-to-recent-article.test.ts b/src/services/graphql/helpers/convert-recent-post-to-recent-article.test.ts
new file mode 100644
index 0000000..8acf753
--- /dev/null
+++ b/src/services/graphql/helpers/convert-recent-post-to-recent-article.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from '@jest/globals';
+import type { RecentWPPost } from '../../../types';
+import { convertRecentPostToRecentArticle } from './convert-recent-post-to-recent-article';
+import { convertWPImgToImg } from './convert-wp-image-to-img';
+
+describe('convert-recent-post-to-recent-article', () => {
+ it('converts a RecentWPPost object to a RecentArticle object', () => {
+ const post: RecentWPPost = {
+ databaseId: 5,
+ date: '2022-03-20',
+ featuredImage: null,
+ slug: '/the-post-slug',
+ title: 'veritatis ex autem',
+ };
+ const result = convertRecentPostToRecentArticle(post);
+
+ expect(result.cover).toBeUndefined();
+ expect(result.id).toBe(post.databaseId);
+ expect(result.publicationDate).toBe(post.date);
+ expect(result.slug).toBe(post.slug);
+ expect(result.title).toBe(post.title);
+ });
+
+ it('can convert the cover', () => {
+ const post = {
+ databaseId: 5,
+ date: '2022-03-20',
+ featuredImage: {
+ node: {
+ altText: 'molestiae praesentium animi',
+ mediaDetails: {
+ height: 480,
+ width: 640,
+ },
+ sourceUrl: 'https://picsum.photos/640/480',
+ title: 'ullam deserunt perspiciatis',
+ },
+ },
+ slug: '/the-post-slug',
+ title: 'veritatis ex autem',
+ } satisfies RecentWPPost;
+ const result = convertRecentPostToRecentArticle(post);
+
+ expect(result.cover).toStrictEqual(
+ convertWPImgToImg(post.featuredImage.node)
+ );
+ });
+});
diff --git a/src/services/graphql/helpers/convert-recent-post-to-recent-article.ts b/src/services/graphql/helpers/convert-recent-post-to-recent-article.ts
new file mode 100644
index 0000000..ff5eb67
--- /dev/null
+++ b/src/services/graphql/helpers/convert-recent-post-to-recent-article.ts
@@ -0,0 +1,24 @@
+import type { RecentArticle, RecentWPPost } from '../../../types';
+import { convertWPImgToImg } from './convert-wp-image-to-img';
+
+/**
+ * Convert a WordPress post to an article.
+ *
+ * @param {RecentWPPost} post - A post.
+ * @returns {RecentArticle} An article.
+ */
+export const convertRecentPostToRecentArticle = ({
+ databaseId,
+ date,
+ featuredImage,
+ slug,
+ title,
+}: RecentWPPost): RecentArticle => {
+ return {
+ cover: featuredImage ? convertWPImgToImg(featuredImage.node) : undefined,
+ id: databaseId,
+ publicationDate: date,
+ slug,
+ title,
+ };
+};
diff --git a/src/services/graphql/helpers/convert-taxonomy-to-page-link.test.ts b/src/services/graphql/helpers/convert-taxonomy-to-page-link.test.ts
new file mode 100644
index 0000000..b687ccb
--- /dev/null
+++ b/src/services/graphql/helpers/convert-taxonomy-to-page-link.test.ts
@@ -0,0 +1,52 @@
+import { describe, expect, it } from '@jest/globals';
+import type { WPThematicPreview, WPTopicPreview } from '../../../types';
+import { convertTaxonomyToPageLink } from './convert-taxonomy-to-page-link';
+
+describe('convert-taxonomy-to-page-link', () => {
+ it('can convert a WPThematicPreview object to a Thematic object', () => {
+ const thematic: WPThematicPreview = {
+ databaseId: 42,
+ slug: '/the-thematic-slug',
+ title: 'et ut alias',
+ };
+ const result = convertTaxonomyToPageLink(thematic);
+
+ expect(result.id).toBe(thematic.databaseId);
+ expect(result.logo).toBeUndefined();
+ expect(result.name).toBe(thematic.title);
+ expect(result.url).toBe(thematic.slug);
+ });
+
+ it('can convert a WPTopicPreview object to a Topic object', () => {
+ const topic: WPTopicPreview = {
+ databaseId: 42,
+ featuredImage: {
+ node: {
+ altText: 'dolorem quia possimus',
+ mediaDetails: {
+ height: 480,
+ width: 640,
+ },
+ sourceUrl: 'https://picsum.photos/640/480',
+ title: 'eos',
+ },
+ },
+ slug: '/the-topic-slug',
+ title: 'et ut alias',
+ };
+ const result = convertTaxonomyToPageLink(topic);
+
+ expect(result.id).toBe(topic.databaseId);
+ expect(result.logo?.alt).toBe(topic.featuredImage?.node.altText);
+ expect(result.logo?.height).toBe(
+ topic.featuredImage?.node.mediaDetails.height
+ );
+ expect(result.logo?.src).toBe(topic.featuredImage?.node.sourceUrl);
+ expect(result.logo?.title).toBe(topic.featuredImage?.node.title);
+ expect(result.logo?.width).toBe(
+ topic.featuredImage?.node.mediaDetails.width
+ );
+ expect(result.name).toBe(topic.title);
+ expect(result.url).toBe(topic.slug);
+ });
+});
diff --git a/src/services/graphql/helpers/convert-taxonomy-to-page-link.ts b/src/services/graphql/helpers/convert-taxonomy-to-page-link.ts
new file mode 100644
index 0000000..2294fb7
--- /dev/null
+++ b/src/services/graphql/helpers/convert-taxonomy-to-page-link.ts
@@ -0,0 +1,23 @@
+import type {
+ PageLink,
+ WPThematicPreview,
+ WPTopicPreview,
+} from '../../../types';
+import { convertWPImgToImg } from './convert-wp-image-to-img';
+
+export const convertTaxonomyToPageLink = ({
+ databaseId,
+ slug,
+ title,
+ ...props
+}: WPThematicPreview | WPTopicPreview): PageLink => {
+ return {
+ id: databaseId,
+ logo:
+ 'featuredImage' in props && props.featuredImage
+ ? convertWPImgToImg(props.featuredImage.node)
+ : undefined,
+ name: title,
+ url: slug,
+ };
+};
diff --git a/src/services/graphql/helpers/convert-wp-comment-to-comment.test.ts b/src/services/graphql/helpers/convert-wp-comment-to-comment.test.ts
new file mode 100644
index 0000000..4b385b4
--- /dev/null
+++ b/src/services/graphql/helpers/convert-wp-comment-to-comment.test.ts
@@ -0,0 +1,93 @@
+import { describe, expect, it } from '@jest/globals';
+import type { WPComment } from '../../../types';
+import { convertWPCommentToComment } from './convert-wp-comment-to-comment';
+
+describe('convert-wp-comment-to-comment', () => {
+ it('converts a WPComment object to a Comment object', () => {
+ const comment: WPComment = {
+ approved: true,
+ author: {
+ node: {
+ avatar: null,
+ name: 'Maribel.Roberts',
+ url: null,
+ },
+ },
+ content: 'Aliquam qui et facere consequatur quia.',
+ databaseId: 4,
+ date: '2023-10-15',
+ parentDatabaseId: 1,
+ status: 'HOLD',
+ };
+
+ const transformedComment = convertWPCommentToComment(comment);
+
+ expect(transformedComment.content).toBe(comment.content);
+ expect(transformedComment.id).toBe(comment.databaseId);
+ expect(transformedComment.isApproved).toBe(comment.approved);
+ expect(transformedComment.meta.author.avatar).toBeUndefined();
+ expect(transformedComment.meta.author.name).toBe(comment.author.node.name);
+ expect(transformedComment.meta.author.website).toBeUndefined();
+ expect(transformedComment.parentId).toBe(comment.parentDatabaseId);
+ expect(transformedComment.replies).toStrictEqual([]);
+ });
+
+ it('can convert the avatar', () => {
+ const comment: WPComment = {
+ approved: true,
+ author: {
+ node: {
+ avatar: {
+ height: 80,
+ url: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/426.jpg',
+ width: 80,
+ },
+ name: 'Maribel.Roberts',
+ url: null,
+ },
+ },
+ content: 'Aliquam qui et facere consequatur quia.',
+ databaseId: 4,
+ date: '2023-10-15',
+ parentDatabaseId: 1,
+ status: 'HOLD',
+ };
+
+ const transformedComment = convertWPCommentToComment(comment);
+
+ expect(transformedComment.meta.author.avatar?.alt).toBe(
+ `${comment.author.node.name} avatar`
+ );
+ expect(transformedComment.meta.author.avatar?.height).toBe(
+ comment.author.node.avatar?.height
+ );
+ expect(transformedComment.meta.author.avatar?.src).toBe(
+ comment.author.node.avatar?.url
+ );
+ expect(transformedComment.meta.author.avatar?.width).toBe(
+ comment.author.node.avatar?.width
+ );
+ });
+
+ it('can remove the parentId when not meaningful', () => {
+ const comment: WPComment = {
+ approved: true,
+ author: {
+ node: {
+ avatar: null,
+ name: 'Maribel.Roberts',
+ url: null,
+ },
+ },
+ content: 'Aliquam qui et facere consequatur quia.',
+ databaseId: 4,
+ date: '2023-10-15',
+ parentDatabaseId: 0,
+ status: 'HOLD',
+ };
+
+ const transformedComment = convertWPCommentToComment(comment);
+
+ expect(transformedComment.parentId).toBeUndefined();
+ });
+});
diff --git a/src/services/graphql/helpers/convert-wp-comment-to-comment.ts b/src/services/graphql/helpers/convert-wp-comment-to-comment.ts
new file mode 100644
index 0000000..7a7e2ca
--- /dev/null
+++ b/src/services/graphql/helpers/convert-wp-comment-to-comment.ts
@@ -0,0 +1,35 @@
+import type { SingleComment, WPComment } from '../../../types';
+
+/**
+ * Convert a comment from WordPress type to SingleComment.
+ *
+ * @param {WPComment} comment - A raw comment from WordPress.
+ * @returns {SingleComment} A comment.
+ */
+export const convertWPCommentToComment = (
+ comment: WPComment
+): SingleComment => {
+ return {
+ content: comment.content,
+ isApproved: comment.approved,
+ id: comment.databaseId,
+ meta: {
+ author: {
+ name: comment.author.node.name,
+ avatar: comment.author.node.avatar
+ ? {
+ alt: `${comment.author.node.name} avatar`,
+ height: comment.author.node.avatar.height,
+ src: comment.author.node.avatar.url,
+ width: comment.author.node.avatar.width,
+ }
+ : undefined,
+ website: comment.author.node.url ?? undefined,
+ },
+ date: comment.date,
+ },
+ parentId:
+ comment.parentDatabaseId === 0 ? undefined : comment.parentDatabaseId,
+ replies: [],
+ };
+};
diff --git a/src/services/graphql/helpers/convert-wp-image-to-img.test.ts b/src/services/graphql/helpers/convert-wp-image-to-img.test.ts
new file mode 100644
index 0000000..ca58a4f
--- /dev/null
+++ b/src/services/graphql/helpers/convert-wp-image-to-img.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from '@jest/globals';
+import type { WPImage } from '../../../types';
+import { convertWPImgToImg } from './convert-wp-image-to-img';
+
+describe('convert-wp-image-to-img', () => {
+ it('converts a WPImage object to an Img object', () => {
+ const img: WPImage = {
+ altText: 'molestiae praesentium animi',
+ mediaDetails: {
+ height: 480,
+ width: 640,
+ },
+ sourceUrl: 'https://picsum.photos/640/480',
+ title: null,
+ };
+
+ const transformedImg = convertWPImgToImg(img);
+
+ expect(transformedImg.alt).toBe(img.altText);
+ expect(transformedImg.height).toBe(img.mediaDetails.height);
+ expect(transformedImg.src).toBe(img.sourceUrl);
+ expect(transformedImg.title).toBeUndefined();
+ expect(transformedImg.width).toBe(img.mediaDetails.width);
+ });
+
+ it('can return an empty string if altText is missing', () => {
+ const img: WPImage = {
+ altText: null,
+ mediaDetails: {
+ height: 480,
+ width: 640,
+ },
+ sourceUrl: 'https://picsum.photos/640/480',
+ title: null,
+ };
+
+ const transformedImg = convertWPImgToImg(img);
+
+ expect(transformedImg.alt).toBe('');
+ });
+});
diff --git a/src/services/graphql/helpers/convert-wp-image-to-img.ts b/src/services/graphql/helpers/convert-wp-image-to-img.ts
new file mode 100644
index 0000000..392aaf9
--- /dev/null
+++ b/src/services/graphql/helpers/convert-wp-image-to-img.ts
@@ -0,0 +1,16 @@
+import type { Img, WPImage } from '../../../types';
+
+export const convertWPImgToImg = ({
+ altText,
+ mediaDetails,
+ sourceUrl,
+ title,
+}: WPImage): Img => {
+ return {
+ alt: altText ?? '',
+ height: mediaDetails.height,
+ src: sourceUrl,
+ title: title ?? undefined,
+ width: mediaDetails.width,
+ };
+};
diff --git a/src/services/graphql/helpers/index.ts b/src/services/graphql/helpers/index.ts
new file mode 100644
index 0000000..16e93d2
--- /dev/null
+++ b/src/services/graphql/helpers/index.ts
@@ -0,0 +1,7 @@
+export * from './build-comments-tree';
+export * from './convert-post-preview-to-article-preview';
+export * from './convert-post-to-article';
+export * from './convert-recent-post-to-recent-article';
+export * from './convert-taxonomy-to-page-link';
+export * from './convert-wp-comment-to-comment';
+export * from './convert-wp-image-to-img';
diff --git a/src/services/graphql/index.ts b/src/services/graphql/index.ts
index c1eac16..53afbc7 100644
--- a/src/services/graphql/index.ts
+++ b/src/services/graphql/index.ts
@@ -1,12 +1,3 @@
-export * from './api';
-export * from './articles';
-export * from './articles.query';
-export * from './comments';
-export * from './comments.mutation';
-export * from './comments.query';
-export * from './contact';
-export * from './contact.mutation';
-export * from './thematics';
-export * from './thematics.query';
-export * from './topics';
-export * from './topics.query';
+export * from './fetchers';
+export * from './helpers';
+export * from './mutators';
diff --git a/src/services/graphql/mutators/create-comment.ts b/src/services/graphql/mutators/create-comment.ts
new file mode 100644
index 0000000..d9d177d
--- /dev/null
+++ b/src/services/graphql/mutators/create-comment.ts
@@ -0,0 +1,70 @@
+import type { Nullable } from '../../../types';
+import { fetchGraphQL, getGraphQLUrl } from '../../../utils/helpers';
+
+type CreatedComment = {
+ clientMutationId: string;
+ success: boolean;
+ comment: Nullable<{
+ approved: boolean;
+ }>;
+};
+
+type CreateCommentResponse = {
+ createComment: CreatedComment;
+};
+
+export const createCommentMutation = `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
+ }
+ }
+}`;
+
+export type CreateCommentInput = {
+ author: string;
+ authorEmail: string;
+ authorUrl: string;
+ clientMutationId: string;
+ commentOn: number;
+ content: string;
+ parent?: number;
+};
+
+/**
+ * Create a new comment using GraphQL API.
+ *
+ * @param {CreateCommentInput} input - The comment data.
+ * @returns {Promise<CreatedComment>} The created comment.
+ */
+export const createComment = async (
+ input: CreateCommentInput
+): Promise<CreatedComment> => {
+ const response = await fetchGraphQL<CreateCommentResponse>({
+ query: createCommentMutation,
+ url: getGraphQLUrl(),
+ variables: { ...input },
+ });
+
+ return response.createComment;
+};
diff --git a/src/services/graphql/mutators/index.ts b/src/services/graphql/mutators/index.ts
new file mode 100644
index 0000000..dfdd511
--- /dev/null
+++ b/src/services/graphql/mutators/index.ts
@@ -0,0 +1,2 @@
+export * from './create-comment';
+export * from './send-email';
diff --git a/src/services/graphql/mutators/send-email.ts b/src/services/graphql/mutators/send-email.ts
new file mode 100644
index 0000000..45b6fca
--- /dev/null
+++ b/src/services/graphql/mutators/send-email.ts
@@ -0,0 +1,49 @@
+import { fetchGraphQL, getGraphQLUrl } from 'src/utils/helpers';
+
+type SentEmail = {
+ clientMutationId: string;
+ message: string;
+ origin: string;
+ replyTo: string;
+ sent: boolean;
+};
+
+type SendEmailResponse = {
+ sendEmail: SentEmail;
+};
+
+const sendMailMutation = `mutation SendEmail($body: String, $clientMutationId: String, $replyTo: String, $subject: String) {
+ sendEmail(
+ input: {body: $body, clientMutationId: $clientMutationId, replyTo: $replyTo, subject: $subject}
+ ) {
+ clientMutationId
+ message
+ origin
+ replyTo
+ sent
+ to
+ }
+}`;
+
+export type SendMailInput = {
+ body: string;
+ clientMutationId: string;
+ replyTo: string;
+ subject: string;
+};
+
+/**
+ * Send an email using GraphQL API.
+ *
+ * @param {SendMailInput} data - The mail data.
+ * @returns {Promise<SentEmail>} The mutation response.
+ */
+export const sendMail = async (data: SendMailInput): Promise<SentEmail> => {
+ const response = await fetchGraphQL<SendEmailResponse>({
+ query: sendMailMutation,
+ url: getGraphQLUrl(),
+ variables: { ...data },
+ });
+
+ return response.sendEmail;
+};
diff --git a/src/services/graphql/thematics.query.ts b/src/services/graphql/thematics.query.ts
deleted file mode 100644
index 5a82133..0000000
--- a/src/services/graphql/thematics.query.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-/**
- * 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
deleted file mode 100644
index c02a42c..0000000
--- a/src/services/graphql/thematics.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import type {
- EdgesResponse,
- GraphQLEdgesInput,
- PageLink,
- RawArticle,
- RawThematic,
- RawThematicPreview,
- Slug,
- Thematic,
- TotalItems,
-} from '../../types';
-import {
- getImageFromRawData,
- getPageLinkFromRawData,
- sortPageLinksByName,
-} from '../../utils/helpers';
-import { fetchAPI } 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>({
- query: totalThematicsQuery,
- });
-
- return response.thematics.pageInfo.total;
-};
-
-/**
- * Retrieve the given number of thematics from API.
- *
- * @param {GraphQLEdgesInput} props - An object of GraphQL variables.
- * @returns {Promise<EdgesResponse<RawThematicPreview>>} The thematics data.
- */
-export const getThematicsPreview = async (
- props: GraphQLEdgesInput
-): Promise<EdgesResponse<RawThematicPreview>> => {
- const response = await fetchAPI<
- RawThematicPreview,
- typeof thematicsListQuery
- >({ 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 = async (
- data: RawThematic
-): Promise<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) {
- for (const topic of post.acfPosts.postsInTopic) {
- topics.push(getPageLinkFromRawData(topic, 'topic'));
- }
- }
- });
-
- const topicsIds = topics.map((topic) => topic.id);
- const uniqueTopics = topics.filter(
- ({ id }, index) => !topicsIds.includes(id, index + 1)
- );
-
- return uniqueTopics.sort(sortPageLinksByName);
- };
-
- return {
- content: contentParts.afterMore,
- id: databaseId,
- intro: contentParts.beforeMore,
- meta: {
- articles: await Promise.all(
- acfThematics.postsInThematic.map(async (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>({
- 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>({
- 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
deleted file mode 100644
index 57b2569..0000000
--- a/src/services/graphql/topics.query.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * 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
deleted file mode 100644
index d8a9b6a..0000000
--- a/src/services/graphql/topics.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-import type {
- EdgesResponse,
- GraphQLEdgesInput,
- PageLink,
- RawArticle,
- RawTopic,
- RawTopicPreview,
- Slug,
- Topic,
- TotalItems,
-} from '../../types';
-import {
- getImageFromRawData,
- getPageLinkFromRawData,
- sortPageLinksByName,
-} from '../../utils/helpers';
-import { fetchAPI } 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>({
- query: totalTopicsQuery,
- });
-
- return response.topics.pageInfo.total;
-};
-
-/**
- * Retrieve the given number of topics from API.
- *
- * @param {GraphQLEdgesInput} props - An object of GraphQL variables.
- * @returns {Promise<EdgesResponse<RawTopicPreview>>} The topics data.
- */
-export const getTopicsPreview = async (
- props: GraphQLEdgesInput
-): Promise<EdgesResponse<RawTopicPreview>> => {
- const response = await fetchAPI<RawTopicPreview, typeof topicsListQuery>({
- 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 = async (data: RawTopic): Promise<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) {
- for (const thematic of post.acfPosts.postsInThematic) {
- thematics.push(getPageLinkFromRawData(thematic, 'thematic'));
- }
- }
- });
-
- const thematicsIds = thematics.map((thematic) => thematic.id);
- const uniqueThematics = thematics.filter(
- ({ id }, index) => !thematicsIds.includes(id, index + 1)
- );
-
- return uniqueThematics.sort(sortPageLinksByName);
- };
-
- return {
- content: contentParts.afterMore,
- id: databaseId,
- intro: contentParts.beforeMore,
- meta: {
- articles: await Promise.all(
- acfTopics.postsInTopic.map(async (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>({
- 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>({
- query: topicsSlugQuery,
- variables: { first: totalTopics },
- });
-
- return response.topics.edges.map((edge) => edge.node.slug);
-};