Skip to content

Commit

Permalink
Merge pull request #236 from wunderio/feature/NEX-101-data-fetching
Browse files Browse the repository at this point in the history
NEX-101: Improve data fetching and error handling
  • Loading branch information
joshua-scott authored Jul 30, 2024
2 parents ef29615 + 586757f commit 99b7462
Showing 14 changed files with 208 additions and 159 deletions.
2 changes: 2 additions & 0 deletions next/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const REVALIDATE_SHORT = 10; // 10 seconds
export const REVALIDATE_LONG = 60 * 10; // 10 minutes
4 changes: 2 additions & 2 deletions next/lib/contexts/language-links-context.tsx
Original file line number Diff line number Diff line change
@@ -37,10 +37,10 @@ export const getStandardLanguageLinks = () =>
*/
export function createLanguageLinksForNextOnlyPage(
path: string,
context: GetStaticPropsContext,
locales: GetStaticPropsContext["locales"],
): LanguageLinks {
const languageLinks = getStandardLanguageLinks();
context.locales.forEach((locale) => {
locales.forEach((locale) => {
languageLinks[locale].path =
languageLinks[locale].path === "/"
? path
10 changes: 8 additions & 2 deletions next/lib/drupal/drupal-client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { DrupalClient } from "next-drupal";
import { type TypedDocumentNode } from "@graphql-typed-document-node/core";
import { request, type RequestDocument, type Variables } from "graphql-request";
import pRetry, { type Options } from "p-retry";
import pRetry, { AbortError, type Options } from "p-retry";

import { env } from "@/env";

const RETRY_OPTIONS: Options = {
retries: env.NODE_ENV === "development" ? 1 : 5,
onFailedAttempt: ({ attemptNumber, retriesLeft }) => {
onFailedAttempt: ({ attemptNumber, retriesLeft, message }) => {
// Don't retry GraphQL errors:
if (message.startsWith("GraphQL Error")) {
// Throw the relevant part of the error message (e.g. "GraphQL Error (Code: 500)")
throw new AbortError(message.slice(0, 25));
}

console.log(
`Fetch ${attemptNumber} failed (${retriesLeft} retries remaining)`,
);
12 changes: 9 additions & 3 deletions next/lib/get-common-page-props.ts
Original file line number Diff line number Diff line change
@@ -3,12 +3,18 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations";

import { getMenus } from "@/lib/drupal/get-menus";

import siteConfig from "@/site.config";

export type CommonPageProps = Awaited<ReturnType<typeof getCommonPageProps>>;

export async function getCommonPageProps(context: GetStaticPropsContext) {
export async function getCommonPageProps({
locale = siteConfig.defaultLocale,
}: {
locale: GetStaticPropsContext["locale"];
}) {
const [translations, menus] = await Promise.all([
serverSideTranslations(context.locale),
getMenus(context),
serverSideTranslations(locale),
getMenus({ locale }),
]);

return {
11 changes: 6 additions & 5 deletions next/pages/404.tsx
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import { useTranslation } from "next-i18next";

import { HeadingPage } from "@/components/heading--page";
import { Meta } from "@/components/meta";
import { REVALIDATE_LONG } from "@/lib/constants";
import {
CommonPageProps,
getCommonPageProps,
@@ -25,13 +26,13 @@ export default function NotFoundPage() {
);
}

export const getStaticProps: GetStaticProps<CommonPageProps> = async (
context,
) => {
export const getStaticProps: GetStaticProps<CommonPageProps> = async ({
locale,
}) => {
return {
props: {
...(await getCommonPageProps(context)),
...(await getCommonPageProps({ locale })),
},
revalidate: 60,
revalidate: REVALIDATE_LONG,
};
};
11 changes: 6 additions & 5 deletions next/pages/500.tsx
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import { useTranslation } from "next-i18next";

import { HeadingPage } from "@/components/heading--page";
import { Meta } from "@/components/meta";
import { REVALIDATE_LONG } from "@/lib/constants";
import {
CommonPageProps,
getCommonPageProps,
@@ -25,13 +26,13 @@ export default function NotFoundPage() {
);
}

export const getStaticProps: GetStaticProps<CommonPageProps> = async (
context,
) => {
export const getStaticProps: GetStaticProps<CommonPageProps> = async ({
locale,
}) => {
return {
props: {
...(await getCommonPageProps(context)),
...(await getCommonPageProps({ locale })),
},
revalidate: 60,
revalidate: REVALIDATE_LONG,
};
};
161 changes: 87 additions & 74 deletions next/pages/[...slug].tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { PreviewData, Redirect } from "next";
import type { GetStaticPathsResult, Redirect } from "next";
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
import { AbortError } from "p-retry";

import { Meta } from "@/components/meta";
import { Node } from "@/components/node";
import { REVALIDATE_LONG, REVALIDATE_SHORT } from "@/lib/constants";
import {
createLanguageLinks,
LanguageLinks,
@@ -27,6 +29,8 @@ import {
} from "@/lib/graphql/utils";
import { TypedRouteEntity } from "@/types/graphql";

import { env } from "@/env";

export default function CustomPage({
node,
}: InferGetStaticPropsType<typeof getStaticProps>) {
@@ -45,13 +49,8 @@ export default function CustomPage({
);
}

export const getStaticPaths: GetStaticPaths = async (context) => {
// We want to generate static paths for all locales:
const locales = context.locales || [];

const staticPaths: ReturnType<
typeof drupalClientViewer.buildStaticPathsParamsFromPaths
> = [];
export const getStaticPaths: GetStaticPaths = async ({ locales }) => {
const staticPaths: GetStaticPathsResult["paths"] = [];

for (const locale of locales) {
// Get the defined paths via graphql for the current locale:
@@ -92,31 +91,45 @@ interface PageProps extends CommonPageProps {
languageLinks: LanguageLinks;
}

export const getStaticProps: GetStaticProps<PageProps> = async (context) => {
// Get the path from the context:
const path = Array.isArray(context.params.slug)
? `/${context.params.slug?.join("/")}`
: context.params.slug;

const variables = {
path: path,
langcode: context.locale,
};

// Are we in Next.js preview mode?
const isPreview = context.preview || false;
const drupalClient = isPreview ? drupalClientPreviewer : drupalClientViewer;

// Get the page data with Graphql.
// We want to use a different client if we are in preview mode:
const data = await drupalClient.doGraphQlRequest(
GET_ENTITY_AT_DRUPAL_PATH,
variables,
);

// If the data contains a RedirectResponse, we redirect to the path:
const redirect = extractRedirectFromRouteQueryResult(data);
export const getStaticProps: GetStaticProps<PageProps> = async ({
locale,
defaultLocale,
params,
preview,
previewData,
revalidateReason,
}) => {
const commonPageProps = getCommonPageProps({ locale });

const drupalClient = preview ? drupalClientPreviewer : drupalClientViewer;
const path = "/" + (params.slug as string[]).join("/");

const nodeByPathResult = await drupalClient
.doGraphQlRequest(GET_ENTITY_AT_DRUPAL_PATH, {
path,
langcode: locale,
})
.catch((error: unknown) => {
const type =
error instanceof AbortError
? "GraphQL"
: error instanceof TypeError
? "Network"
: "Unknown";

const moreInfo =
type === "GraphQL"
? `Check graphql_compose logs: ${env.NEXT_PUBLIC_DRUPAL_BASE_URL}/admin/reports`
: "";

throw new Error(
`${type} Error during GetNodeByPath query with $path: "${path}" and $langcode: "${locale}". ${moreInfo}`,
);
});

// The response will contain either a redirect or node data.
// If it's a redirect, redirect to the new path:
const redirect = extractRedirectFromRouteQueryResult(nodeByPathResult);
if (redirect) {
return {
redirect: {
@@ -128,90 +141,90 @@ export const getStaticProps: GetStaticProps<PageProps> = async (context) => {
};
}

// Get the entity from the response:
let nodeEntity = extractEntityFromRouteQueryResult(data);
let node = extractEntityFromRouteQueryResult(nodeByPathResult);

// If there's no node, return 404:
if (!nodeEntity) {
// Node not found:
if (!node) {
switch (revalidateReason) {
case "build":
// Pages returned from getStaticPaths should always exist. Abort the build:
throw new Error(
`Node not found in GetNodeByPath query response with $path: "${path}" and $langcode: "${locale}".`,
);
case "stale":
case "on-demand":
default:
// Not an error, the requested node just doesn't exist. Return 404:
return {
notFound: true,
revalidate: REVALIDATE_LONG,
};
}
}

// Node is not published:
if (!preview && node.status !== true) {
return {
notFound: true,
revalidate: 60,
revalidate: REVALIDATE_LONG,
};
}

// If node is a frontpage, redirect to / for the current locale:
if (nodeEntity.__typename === "NodeFrontpage") {
// Node is actually a frontpage:
if (!preview && node.__typename === "NodeFrontpage") {
return {
redirect: {
destination: `/${context.locale}`,
destination: locale === defaultLocale ? "/" : `/${locale}`,
permanent: false,
} satisfies Redirect,
revalidate: REVALIDATE_LONG,
};
}

// When in preview, we could be requesting a specific revision.
// In this case, the previewData will contain the resourceVersion property,
// we can use that in combination with the node id to fetch the correct revision
// This means that we will need to do a second request to Drupal.
const { previewData } = context as {
previewData: PreviewData & { resourceVersion?: string };
};
// which we can use to fetch the correct revision:
if (
isPreview &&
previewData &&
preview &&
typeof previewData === "object" &&
previewData.resourceVersion &&
// If the resourceVersion is "rel:latest-version", we don't need to fetch the revision:
"resourceVersion" in previewData &&
typeof previewData.resourceVersion === "string" &&
previewData.resourceVersion !== "rel:latest-version"
) {
// Get the node id from the entity we already have:
const nodeId = nodeEntity.id;
// the revision will be in the format "id:[id]":
const revisionId = previewData.resourceVersion.split(":").slice(1);
// To fetch the entity at a specific revision, we need to call a specific path:
const revisionPath = `/node/${nodeId}/revisions/${revisionId}/view`;

// Get the node at the specific data with Graphql:
const [nodeId, revisionId] = previewData.resourceVersion.split(":");
const path = `/node/${nodeId}/revisions/${revisionId}/view`;
const revisionRoutedata = await drupalClient.doGraphQlRequest(
GET_ENTITY_AT_DRUPAL_PATH,
{ path: revisionPath, langcode: context.locale },
{ path, langcode: locale },
);

// Instead of the entity at the current revision, we want now to
// display the entity at the requested revision:
nodeEntity = extractEntityFromRouteQueryResult(revisionRoutedata);
if (!nodeEntity) {
node = extractEntityFromRouteQueryResult(revisionRoutedata);
if (!node) {
return {
notFound: true,
revalidate: 60,
revalidate: REVALIDATE_SHORT,
};
}
}

// Unless we are in preview, return 404 if the node is set to unpublished:
if (!isPreview && nodeEntity.status !== true) {
return {
notFound: true,
revalidate: 60,
};
}

// Add information about possible other language versions of this node.
let languageLinks;
// Not all node types necessarily have translations enabled,
// if so, only show the standard language links.
if ("translations" in nodeEntity) {
languageLinks = createLanguageLinks(nodeEntity.translations);
if ("translations" in node) {
languageLinks = createLanguageLinks(node.translations);
} else {
languageLinks = getStandardLanguageLinks();
}

return {
props: {
...(await getCommonPageProps(context)),
node: nodeEntity,
...(await commonPageProps),
node,
languageLinks,
},
revalidate: 60,
revalidate: REVALIDATE_LONG,
};
};
Loading

0 comments on commit 99b7462

Please sign in to comment.