Distributed cache * cache deleteKey now uses an options object instead of a lonely argument variable fuzzy * merge * remove debug logs and cleanup * cleanup * add fault handling * add fault handling * add pid when logging redis client creation * add identifier when logging redis client creation * cleanup * feat: add redis-api as it's own app * feature: use http wrapper for redis * feat: add the possibility to fallback to unstable_cache * Add error handling if redis cache is unresponsive * add logging for unstable_cache * merge * don't cache errors * fix: metadatabase on branchdeploys * Handle when /en/destinations throws add ErrorBoundary * Add sentry-logging when ErrorBoundary catches exception * Fix error handling for distributed cache * cleanup code * Added Application Insights back * Update generateApiKeys script and remove duplicate * Merge branch 'feature/redis' of bitbucket.org:scandic-swap/web into feature/redis * merge Approved-by: Linus Flood
277 lines
9.2 KiB
TypeScript
277 lines
9.2 KiB
TypeScript
import { metrics } from "@opentelemetry/api"
|
|
import { cache } from "react"
|
|
|
|
import { GetAccountPageMetadata } from "@/lib/graphql/Query/AccountPage/Metadata.graphql"
|
|
import { GetCollectionPageMetadata } from "@/lib/graphql/Query/CollectionPage/Metadata.graphql"
|
|
import { GetContentPageMetadata } from "@/lib/graphql/Query/ContentPage/Metadata.graphql"
|
|
import { GetDestinationCityPageMetadata } from "@/lib/graphql/Query/DestinationCityPage/Metadata.graphql"
|
|
import { GetDestinationCountryPageMetadata } from "@/lib/graphql/Query/DestinationCountryPage/Metadata.graphql"
|
|
import { GetDestinationOverviewPageMetadata } from "@/lib/graphql/Query/DestinationOverviewPage/Metadata.graphql"
|
|
import { GetHotelPageMetadata } from "@/lib/graphql/Query/HotelPage/Metadata.graphql"
|
|
import { GetLoyaltyPageMetadata } from "@/lib/graphql/Query/LoyaltyPage/Metadata.graphql"
|
|
import { GetStartPageMetadata } from "@/lib/graphql/Query/StartPage/Metadata.graphql"
|
|
import { request } from "@/lib/graphql/request"
|
|
import { notFound } from "@/server/errors/trpc"
|
|
import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
|
|
|
|
import { generateTag } from "@/utils/generateTag"
|
|
|
|
import { getHotel } from "../../hotels/query"
|
|
import { getUrlsOfAllLanguages } from "../languageSwitcher/utils"
|
|
import { getMetadataInput } from "./input"
|
|
import { getNonContentstackUrls, metadataSchema } from "./output"
|
|
import { affix, getCityData, getCountryData } from "./utils"
|
|
|
|
import type { Metadata } from "next"
|
|
|
|
import { PageContentTypeEnum } from "@/types/requests/contentType"
|
|
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
|
|
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
|
|
import type { Lang } from "@/constants/languages"
|
|
|
|
const meter = metrics.getMeter("trpc.metadata")
|
|
|
|
// OpenTelemetry metrics
|
|
const fetchMetadataCounter = meter.createCounter(
|
|
"trpc.contentstack.metadata.get"
|
|
)
|
|
const fetchMetadataSuccessCounter = meter.createCounter(
|
|
"trpc.contentstack.metadata.get-success"
|
|
)
|
|
const fetchMetadataFailCounter = meter.createCounter(
|
|
"trpc.contentstack.metadata.get-fail"
|
|
)
|
|
const transformMetadataCounter = meter.createCounter(
|
|
"trpc.contentstack.metadata.transform"
|
|
)
|
|
const transformMetadataSuccessCounter = meter.createCounter(
|
|
"trpc.contentstack.metadata.transform-success"
|
|
)
|
|
const transformMetadataFailCounter = meter.createCounter(
|
|
"trpc.contentstack.metadata.transform-fail"
|
|
)
|
|
|
|
const fetchMetadata = cache(async function fetchMemoizedMetadata<T>(
|
|
query: string,
|
|
{ uid, lang }: { uid: string; lang: Lang }
|
|
) {
|
|
fetchMetadataCounter.add(1, { lang, uid })
|
|
console.info(
|
|
"contentstack.metadata fetch start",
|
|
JSON.stringify({ query: { lang, uid } })
|
|
)
|
|
const response = await request<T>(
|
|
query,
|
|
{ locale: lang, uid },
|
|
{
|
|
key: generateTag(lang, uid, affix),
|
|
ttl: "max",
|
|
}
|
|
)
|
|
if (!response.data) {
|
|
const notFoundError = notFound(response)
|
|
fetchMetadataFailCounter.add(1, {
|
|
lang,
|
|
uid,
|
|
error_type: "not_found",
|
|
error: JSON.stringify({ code: notFoundError.code }),
|
|
})
|
|
console.error(
|
|
"contentstack.metadata fetch not found error",
|
|
JSON.stringify({
|
|
query: { lang, uid },
|
|
error: { code: notFoundError.code },
|
|
})
|
|
)
|
|
throw notFoundError
|
|
}
|
|
|
|
fetchMetadataSuccessCounter.add(1, { lang, uid })
|
|
console.info(
|
|
"contentstack.metadata fetch success",
|
|
JSON.stringify({ query: { lang, uid } })
|
|
)
|
|
|
|
return response.data
|
|
})
|
|
|
|
async function getTransformedMetadata(
|
|
data: unknown,
|
|
alternates: Metadata["alternates"]
|
|
) {
|
|
transformMetadataCounter.add(1)
|
|
console.info("contentstack.metadata transform start")
|
|
const validatedMetadata = await metadataSchema.safeParseAsync(data)
|
|
|
|
if (!validatedMetadata.success) {
|
|
transformMetadataFailCounter.add(1, {
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(validatedMetadata.error),
|
|
})
|
|
console.error(
|
|
"contentstack.metadata validation error",
|
|
JSON.stringify({
|
|
error: validatedMetadata.error,
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
transformMetadataSuccessCounter.add(1)
|
|
console.info("contentstack.metadata transform success")
|
|
|
|
if (alternates) {
|
|
validatedMetadata.data.alternates = alternates
|
|
}
|
|
return validatedMetadata.data
|
|
}
|
|
|
|
export const metadataQueryRouter = router({
|
|
get: contentStackUidWithServiceProcedure
|
|
.input(getMetadataInput)
|
|
.query(async ({ ctx, input }) => {
|
|
const variables = {
|
|
lang: ctx.lang,
|
|
uid: ctx.uid,
|
|
}
|
|
|
|
let urls: LanguageSwitcherData | null = null
|
|
if (
|
|
input.subpage ||
|
|
input.filterFromUrl ||
|
|
!ctx.uid ||
|
|
!ctx.contentType
|
|
) {
|
|
urls = getNonContentstackUrls(ctx.lang, `${ctx.lang}/${ctx.pathname}`)
|
|
} else {
|
|
urls = await getUrlsOfAllLanguages(ctx.lang, ctx.uid, ctx.contentType)
|
|
}
|
|
|
|
let alternates: Metadata["alternates"] = null
|
|
|
|
if (urls) {
|
|
const languages: Record<string, string> = {}
|
|
Object.entries(urls)
|
|
.filter(([lang]) => lang !== ctx.lang)
|
|
.forEach(([lang, { url }]) => {
|
|
languages[lang] = url
|
|
})
|
|
const canonical = urls[ctx.lang]?.url
|
|
alternates = {
|
|
canonical,
|
|
languages,
|
|
}
|
|
}
|
|
|
|
switch (ctx.contentType) {
|
|
case PageContentTypeEnum.accountPage:
|
|
const accountPageResponse = await fetchMetadata<{
|
|
account_page: RawMetadataSchema
|
|
}>(GetAccountPageMetadata, variables)
|
|
return getTransformedMetadata(
|
|
accountPageResponse.account_page,
|
|
alternates
|
|
)
|
|
case PageContentTypeEnum.collectionPage:
|
|
const collectionPageResponse = await fetchMetadata<{
|
|
collection_page: RawMetadataSchema
|
|
}>(GetCollectionPageMetadata, variables)
|
|
return getTransformedMetadata(
|
|
collectionPageResponse.collection_page,
|
|
alternates
|
|
)
|
|
case PageContentTypeEnum.contentPage:
|
|
const contentPageResponse = await fetchMetadata<{
|
|
content_page: RawMetadataSchema
|
|
}>(GetContentPageMetadata, variables)
|
|
return getTransformedMetadata(
|
|
contentPageResponse.content_page,
|
|
alternates
|
|
)
|
|
case PageContentTypeEnum.destinationOverviewPage:
|
|
const destinationOverviewPageResponse = await fetchMetadata<{
|
|
destination_overview_page: RawMetadataSchema
|
|
}>(GetDestinationOverviewPageMetadata, variables)
|
|
return getTransformedMetadata(
|
|
destinationOverviewPageResponse.destination_overview_page,
|
|
alternates
|
|
)
|
|
case PageContentTypeEnum.destinationCountryPage:
|
|
const destinationCountryPageResponse = await fetchMetadata<{
|
|
destination_country_page: RawMetadataSchema
|
|
}>(GetDestinationCountryPageMetadata, variables)
|
|
const countryData = await getCountryData(
|
|
destinationCountryPageResponse.destination_country_page,
|
|
input,
|
|
ctx.serviceToken,
|
|
ctx.lang
|
|
)
|
|
return getTransformedMetadata(
|
|
{
|
|
...destinationCountryPageResponse.destination_country_page,
|
|
...countryData,
|
|
},
|
|
alternates
|
|
)
|
|
case PageContentTypeEnum.destinationCityPage:
|
|
const destinationCityPageResponse = await fetchMetadata<{
|
|
destination_city_page: RawMetadataSchema
|
|
}>(GetDestinationCityPageMetadata, variables)
|
|
const cityData = await getCityData(
|
|
destinationCityPageResponse.destination_city_page,
|
|
input,
|
|
ctx.serviceToken,
|
|
ctx.lang
|
|
)
|
|
return getTransformedMetadata(
|
|
{
|
|
...destinationCityPageResponse.destination_city_page,
|
|
...cityData,
|
|
},
|
|
alternates
|
|
)
|
|
case PageContentTypeEnum.loyaltyPage:
|
|
const loyaltyPageResponse = await fetchMetadata<{
|
|
loyalty_page: RawMetadataSchema
|
|
}>(GetLoyaltyPageMetadata, variables)
|
|
return getTransformedMetadata(
|
|
loyaltyPageResponse.loyalty_page,
|
|
alternates
|
|
)
|
|
case PageContentTypeEnum.hotelPage:
|
|
const hotelPageResponse = await fetchMetadata<{
|
|
hotel_page: RawMetadataSchema
|
|
}>(GetHotelPageMetadata, variables)
|
|
const hotelPageData = hotelPageResponse.hotel_page
|
|
const hotelData = hotelPageData.hotel_page_id
|
|
? await getHotel(
|
|
{
|
|
hotelId: hotelPageData.hotel_page_id,
|
|
isCardOnlyPayment: false,
|
|
language: ctx.lang,
|
|
},
|
|
ctx.serviceToken
|
|
)
|
|
: null
|
|
|
|
return getTransformedMetadata(
|
|
{
|
|
...hotelPageData,
|
|
hotelData: hotelData?.hotel,
|
|
},
|
|
alternates
|
|
)
|
|
case PageContentTypeEnum.startPage:
|
|
const startPageResponse = await fetchMetadata<{
|
|
start_page: RawMetadataSchema
|
|
}>(GetStartPageMetadata, variables)
|
|
return getTransformedMetadata(
|
|
startPageResponse.start_page,
|
|
alternates
|
|
)
|
|
default:
|
|
return null
|
|
}
|
|
}),
|
|
})
|