diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-05-02 12:55:13 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-05-02 15:09:36 +0200 |
| commit | 9308a6dce03bd0c616e0ba6fec227473aaa44b33 (patch) | |
| tree | a5b9232b6e15b298316383c13d94cd431d078a13 | |
| parent | a208a8f314f697dbd6f85f8be8332bcea0204178 (diff) | |
refactor: rewrite API fetching method and GraphQL queries
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | src/services/graphql/api.ts | 222 | ||||
| -rw-r--r-- | src/services/graphql/articles.query.ts | 174 | ||||
| -rw-r--r-- | src/services/graphql/comments.query.ts | 21 | ||||
| -rw-r--r-- | src/services/graphql/thematics.query.ts | 116 | ||||
| -rw-r--r-- | src/services/graphql/topics.query.ts | 117 | ||||
| -rw-r--r-- | yarn.lock | 23 |
7 files changed, 651 insertions, 23 deletions
diff --git a/package.json b/package.json index 87c7596..5cec983 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "@next/mdx": "^12.1.5", "feed": "^4.2.2", "graphql": "^16.1.0", - "graphql-request": "^4.2.0", "modern-normalize": "^1.1.0", "next": "^12.1.5", "next-sitemap": "^2.5.20", diff --git a/src/services/graphql/api.ts b/src/services/graphql/api.ts new file mode 100644 index 0000000..b0e8d3a --- /dev/null +++ b/src/services/graphql/api.ts @@ -0,0 +1,222 @@ +import { settings } from '@utils/config'; +import { + articleBySlugQuery, + articlesCardQuery, + articlesQuery, + articlesSlugQuery, + totalArticlesQuery, +} from './articles.query'; +import { commentsQuery } from './comments.query'; +import { + thematicBySlugQuery, + thematicsListQuery, + thematicsSlugQuery, +} from './thematics.query'; +import { + topicBySlugQuery, + topicsListQuery, + topicsSlugQuery, +} from './topics.query'; + +export type Queries = + | typeof articlesQuery + | typeof articleBySlugQuery + | typeof articlesCardQuery + | typeof articlesSlugQuery + | typeof commentsQuery + | typeof thematicBySlugQuery + | typeof thematicsListQuery + | typeof thematicsSlugQuery + | typeof topicBySlugQuery + | typeof topicsListQuery + | typeof topicsSlugQuery + | typeof totalArticlesQuery; + +export type ArticleResponse<T> = { + post: T; +}; + +export type ArticlesResponse<T> = { + posts: T; +}; + +export type CommentsResponse<T> = { + comments: T[]; +}; + +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 ResponseMap<T> = { + [articleBySlugQuery]: ArticleResponse<NodesResponse<T>>; + [articlesCardQuery]: ArticlesResponse<NodesResponse<T>>; + [articlesQuery]: ArticlesResponse<EdgesResponse<T>>; + [articlesSlugQuery]: ArticlesResponse<EdgesResponse<T>>; + [commentsQuery]: CommentsResponse<NodesResponse<T>>; + [thematicBySlugQuery]: ThematicResponse<NodesResponse<T>>; + [thematicsListQuery]: ThematicsResponse<EdgesResponse<T>>; + [thematicsSlugQuery]: ThematicsResponse<EdgesResponse<T>>; + [topicBySlugQuery]: TopicResponse<NodesResponse<T>>; + [topicsListQuery]: TopicsResponse<EdgesResponse<T>>; + [topicsSlugQuery]: TopicsResponse<EdgesResponse<T>>; + [totalArticlesQuery]: ArticlesResponse<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 VariablesMap = { + [articleBySlugQuery]: BySlugVar; + [articlesCardQuery]: EdgesVars; + [articlesQuery]: EdgesVars; + [articlesSlugQuery]: EdgesVars; + [commentsQuery]: ByContentIdVar; + [thematicBySlugQuery]: BySlugVar; + [thematicsListQuery]: EdgesVars; + [thematicsSlugQuery]: EdgesVars; + [topicBySlugQuery]: BySlugVar; + [topicsListQuery]: EdgesVars; + [topicsSlugQuery]: EdgesVars; + [totalArticlesQuery]: null; +}; + +export type FetchAPIProps<T extends Queries> = { + /** + * 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>({ + 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..e384aba --- /dev/null +++ b/src/services/graphql/articles.query.ts @@ -0,0 +1,174 @@ +/** + * 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 + slug + title + } + } + } + author { + node { + gravatarUrl + name + url + } + } + commentCount + contentParts { + afterMore + beforeMore + } + databaseId + date + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + info { + readingTime + 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 + } + } + } + author { + node { + name + } + } + commentCount + contentParts { + beforeMore + } + databaseId + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + info { + readingTime + 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 { + posts { + pageInfo { + total + } + } +}`; 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/thematics.query.ts b/src/services/graphql/thematics.query.ts new file mode 100644 index 0000000..76949ad --- /dev/null +++ b/src/services/graphql/thematics.query.ts @@ -0,0 +1,116 @@ +/** + * 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 { + readingTime + wordsCount + } + modified + slug + title + } + } + } + contentParts { + afterMore + beforeMore + } + databaseId + date + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + info { + readingTime + 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 + } + } +}`; diff --git a/src/services/graphql/topics.query.ts b/src/services/graphql/topics.query.ts new file mode 100644 index 0000000..8783799 --- /dev/null +++ b/src/services/graphql/topics.query.ts @@ -0,0 +1,117 @@ +/** + * 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 { + readingTime + wordsCount + } + modified + slug + title + } + } + } + contentParts { + afterMore + beforeMore + } + databaseId + date + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + info { + readingTime + 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 + 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 + } + } +}`; @@ -6310,13 +6310,6 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-fetch@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -7617,11 +7610,6 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-files@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" - integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -8330,15 +8318,6 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graphql-request@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-4.2.0.tgz#063377bc2dd29cc46aed3fddcc65fe97b805ba81" - integrity sha512-uFeMyhhl8ss4LFgjlfPeAn2pqYw+CJto+cjj71uaBYIMMK2jPIqgHm5KEFxUk0YDD41A8Bq31a2b4G2WJBlp2Q== - dependencies: - cross-fetch "^3.1.5" - extract-files "^9.0.0" - form-data "^3.0.0" - graphql@^16.1.0: version "16.3.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.3.0.tgz#a91e24d10babf9e60c706919bb182b53ccdffc05" @@ -11363,7 +11342,7 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" -node-fetch@2.6.7, node-fetch@^2.6.1: +node-fetch@^2.6.1: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== |
