Merged in feature/redis (pull request #1478)
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
This commit is contained in:
committed by
Linus Flood
parent
a8304e543e
commit
fa63b20ed0
@@ -1,14 +1,16 @@
|
||||
import deepmerge from "deepmerge"
|
||||
import { unstable_cache } from "next/cache"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
|
||||
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
||||
import { metrics } from "./metrics"
|
||||
import {
|
||||
type Cities,
|
||||
citiesByCountrySchema,
|
||||
citiesSchema,
|
||||
countriesSchema,
|
||||
@@ -18,12 +20,10 @@ import {
|
||||
import { getHotel } from "./query"
|
||||
|
||||
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type {
|
||||
CitiesGroupedByCountry,
|
||||
CityLocation,
|
||||
HotelLocation,
|
||||
} from "@/types/trpc/routers/hotel/locations"
|
||||
import type { Endpoint } from "@/lib/api/endpoints"
|
||||
|
||||
@@ -58,18 +58,21 @@ export function getPoiGroupByCategoryName(category: string | undefined) {
|
||||
export const locationsAffix = "locations"
|
||||
|
||||
export const TWENTYFOUR_HOURS = 60 * 60 * 24
|
||||
export async function getCity(
|
||||
cityUrl: string,
|
||||
options: RequestOptionsWithOutBody,
|
||||
lang: Lang,
|
||||
relationshipCity: HotelLocation["relationships"]["city"]
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (locationCityUrl: string) {
|
||||
const url = new URL(locationCityUrl)
|
||||
export async function getCity({
|
||||
cityUrl,
|
||||
serviceToken,
|
||||
}: {
|
||||
cityUrl: string
|
||||
serviceToken: string
|
||||
}): Promise<Cities> {
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
cityUrl,
|
||||
async () => {
|
||||
const url = new URL(cityUrl)
|
||||
const cityResponse = await api.get(
|
||||
url.pathname as Endpoint,
|
||||
options,
|
||||
{ headers: { Authorization: `Bearer ${serviceToken}` } },
|
||||
url.searchParams
|
||||
)
|
||||
|
||||
@@ -81,33 +84,44 @@ export async function getCity(
|
||||
const city = citiesSchema.safeParse(cityJson)
|
||||
if (!city.success) {
|
||||
console.info(`Validation of city failed`)
|
||||
console.info(`cityUrl: ${locationCityUrl}`)
|
||||
console.info(`cityUrl: ${cityUrl}`)
|
||||
console.error(city.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return city.data
|
||||
},
|
||||
[cityUrl, `${lang}:${relationshipCity}`],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(cityUrl)
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getCountries(
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams,
|
||||
export async function getCountries({
|
||||
lang,
|
||||
serviceToken,
|
||||
}: {
|
||||
lang: Lang
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (searchParams) {
|
||||
serviceToken: string
|
||||
}) {
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${lang}:${locationsAffix}:countries`,
|
||||
async () => {
|
||||
const params = new URLSearchParams({
|
||||
language: toApiLang(lang),
|
||||
})
|
||||
|
||||
const countryResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.countries,
|
||||
options,
|
||||
searchParams
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!countryResponse.ok) {
|
||||
return null
|
||||
throw new Error("Unable to fetch countries")
|
||||
}
|
||||
|
||||
const countriesJson = await countryResponse.json()
|
||||
@@ -120,114 +134,128 @@ export async function getCountries(
|
||||
|
||||
return countries.data
|
||||
},
|
||||
[`${lang}:${locationsAffix}:countries`, params.toString()],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params)
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getCitiesByCountry(
|
||||
countries: string[],
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams,
|
||||
lang: Lang,
|
||||
onlyPublished = false, // false by default as it might be used in other places
|
||||
affix: string = locationsAffix
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (
|
||||
searchParams: URLSearchParams,
|
||||
searchedCountries: string[]
|
||||
) {
|
||||
const citiesGroupedByCountry: CitiesGroupedByCountry = {}
|
||||
|
||||
await Promise.all(
|
||||
searchedCountries.map(async (country) => {
|
||||
export async function getCitiesByCountry({
|
||||
countries,
|
||||
lang,
|
||||
onlyPublished = false,
|
||||
affix = locationsAffix,
|
||||
serviceToken,
|
||||
}: {
|
||||
countries: string[]
|
||||
lang: Lang
|
||||
onlyPublished?: boolean // false by default as it might be used in other places
|
||||
affix?: string
|
||||
serviceToken: string
|
||||
}): Promise<CitiesGroupedByCountry> {
|
||||
const cacheClient = await getCacheClient()
|
||||
const allCitiesByCountries = await Promise.all(
|
||||
countries.map(async (country) => {
|
||||
return cacheClient.cacheOrGet(
|
||||
`${lang}:${affix}:cities-by-country:${country}`,
|
||||
async () => {
|
||||
const params = new URLSearchParams({
|
||||
language: toApiLang(lang),
|
||||
})
|
||||
const countryResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.Cities.country(country),
|
||||
options,
|
||||
searchParams
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!countryResponse.ok) {
|
||||
return null
|
||||
throw new Error(`Unable to fetch cities by country ${country}`)
|
||||
}
|
||||
|
||||
const countryJson = await countryResponse.json()
|
||||
const citiesByCountry = citiesByCountrySchema.safeParse(countryJson)
|
||||
if (!citiesByCountry.success) {
|
||||
console.info(`Failed to validate Cities by Country payload`)
|
||||
console.error(`Unable to parse cities by country ${country}`)
|
||||
console.error(citiesByCountry.error)
|
||||
return null
|
||||
throw new Error(`Unable to parse cities by country ${country}`)
|
||||
}
|
||||
|
||||
const cities = onlyPublished
|
||||
? citiesByCountry.data.data.filter((city) => city.isPublished)
|
||||
: citiesByCountry.data.data
|
||||
citiesGroupedByCountry[country] = cities
|
||||
return true
|
||||
})
|
||||
return { ...citiesByCountry.data, country }
|
||||
},
|
||||
"1d"
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
return citiesGroupedByCountry
|
||||
},
|
||||
[
|
||||
`${lang}:${affix}:cities-by-country`,
|
||||
params.toString(),
|
||||
JSON.stringify(countries),
|
||||
],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params, countries)
|
||||
const filteredCitiesByCountries = allCitiesByCountries.map((country) => ({
|
||||
...country,
|
||||
data: onlyPublished
|
||||
? country.data.filter((city) => city.isPublished)
|
||||
: country.data,
|
||||
}))
|
||||
|
||||
const groupedCitiesByCountry: CitiesGroupedByCountry =
|
||||
filteredCitiesByCountries.reduce((acc, { country, data }) => {
|
||||
acc[country] = data
|
||||
return acc
|
||||
}, {} as CitiesGroupedByCountry)
|
||||
|
||||
return groupedCitiesByCountry
|
||||
}
|
||||
|
||||
export async function getLocations(
|
||||
lang: Lang,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams,
|
||||
export async function getLocations({
|
||||
lang,
|
||||
citiesByCountry,
|
||||
serviceToken,
|
||||
}: {
|
||||
lang: Lang
|
||||
citiesByCountry: CitiesGroupedByCountry | null
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (
|
||||
searchParams: URLSearchParams,
|
||||
groupedCitiesByCountry: CitiesGroupedByCountry | null
|
||||
) {
|
||||
serviceToken: string
|
||||
}) {
|
||||
const cacheClient = await getCacheClient()
|
||||
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${lang}:locations`.toLowerCase(),
|
||||
async () => {
|
||||
const params = new URLSearchParams({
|
||||
language: toApiLang(lang),
|
||||
})
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.locations,
|
||||
options,
|
||||
searchParams
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
if (apiResponse.status === 401) {
|
||||
return { error: true, cause: "unauthorized" } as const
|
||||
throw new Error("unauthorized")
|
||||
} else if (apiResponse.status === 403) {
|
||||
return { error: true, cause: "forbidden" } as const
|
||||
throw new Error("forbidden")
|
||||
}
|
||||
return null
|
||||
throw new Error("downstream error")
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedLocations = locationsSchema.safeParse(apiJson)
|
||||
if (!verifiedLocations.success) {
|
||||
console.info(`Locations Verification Failed`)
|
||||
console.error(verifiedLocations.error)
|
||||
return null
|
||||
throw new Error("Unable to parse locations")
|
||||
}
|
||||
|
||||
return await Promise.all(
|
||||
verifiedLocations.data.data.map(async (location) => {
|
||||
if (location.type === "cities") {
|
||||
if (groupedCitiesByCountry) {
|
||||
const country = Object.keys(groupedCitiesByCountry).find(
|
||||
(country) => {
|
||||
if (
|
||||
groupedCitiesByCountry[country].find(
|
||||
(loc) => loc.name === location.name
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (citiesByCountry) {
|
||||
const country = Object.keys(citiesByCountry).find((country) =>
|
||||
citiesByCountry[country].find(
|
||||
(loc) => loc.name === location.name
|
||||
)
|
||||
)
|
||||
if (country) {
|
||||
return {
|
||||
@@ -243,12 +271,10 @@ export async function getLocations(
|
||||
}
|
||||
} else if (location.type === "hotels") {
|
||||
if (location.relationships.city?.url) {
|
||||
const city = await getCity(
|
||||
location.relationships.city.url,
|
||||
options,
|
||||
lang,
|
||||
location.relationships.city
|
||||
)
|
||||
const city = await getCity({
|
||||
cityUrl: location.relationships.city.url,
|
||||
serviceToken,
|
||||
})
|
||||
if (city) {
|
||||
return deepmerge(location, {
|
||||
relationships: {
|
||||
@@ -263,44 +289,51 @@ export async function getLocations(
|
||||
})
|
||||
)
|
||||
},
|
||||
[
|
||||
`${lang}:${locationsAffix}`,
|
||||
params.toString(),
|
||||
JSON.stringify(citiesByCountry),
|
||||
],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params, citiesByCountry)
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getHotelIdsByCityId(
|
||||
cityId: string,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (params: URLSearchParams) {
|
||||
metrics.hotelIds.counter.add(1, { params: params.toString() })
|
||||
export async function getHotelIdsByCityId({
|
||||
cityId,
|
||||
serviceToken,
|
||||
}: {
|
||||
cityId: string
|
||||
serviceToken: string
|
||||
}) {
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${cityId}:hotelsByCityId`,
|
||||
async () => {
|
||||
const searchParams = new URLSearchParams({
|
||||
city: cityId,
|
||||
})
|
||||
metrics.hotelIds.counter.add(1, { params: searchParams.toString() })
|
||||
console.info(
|
||||
"api.hotel.hotel-ids start",
|
||||
JSON.stringify({ params: params.toString() })
|
||||
JSON.stringify({ params: searchParams.toString() })
|
||||
)
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.hotels,
|
||||
options,
|
||||
params
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
searchParams
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const responseMessage = await apiResponse.text()
|
||||
metrics.hotelIds.fail.add(1, {
|
||||
params: params.toString(),
|
||||
params: searchParams.toString(),
|
||||
error_type: "http_error",
|
||||
error: responseMessage,
|
||||
})
|
||||
console.error(
|
||||
"api.hotel.hotel-ids fetch error",
|
||||
JSON.stringify({
|
||||
params: params.toString(),
|
||||
params: searchParams.toString(),
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
@@ -309,59 +342,73 @@ export async function getHotelIdsByCityId(
|
||||
})
|
||||
)
|
||||
|
||||
return []
|
||||
throw new Error("Unable to fetch hotelIds by cityId")
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validatedHotelIds = getHotelIdsSchema.safeParse(apiJson)
|
||||
if (!validatedHotelIds.success) {
|
||||
metrics.hotelIds.fail.add(1, {
|
||||
params: params.toString(),
|
||||
params: searchParams.toString(),
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedHotelIds.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotel.hotel-ids validation error",
|
||||
JSON.stringify({
|
||||
params: params.toString(),
|
||||
params: searchParams.toString(),
|
||||
error: validatedHotelIds.error,
|
||||
})
|
||||
)
|
||||
return []
|
||||
|
||||
throw new Error("Unable to parse data for hotelIds by cityId")
|
||||
}
|
||||
|
||||
metrics.hotelIds.success.add(1, { cityId })
|
||||
console.info(
|
||||
"api.hotel.hotel-ids success",
|
||||
JSON.stringify({
|
||||
params: params.toString(),
|
||||
params: searchParams.toString(),
|
||||
response: validatedHotelIds.data,
|
||||
})
|
||||
)
|
||||
|
||||
return validatedHotelIds.data
|
||||
},
|
||||
[`hotelsByCityId`, params.toString()],
|
||||
{ revalidate: env.CACHE_TIME_HOTELS }
|
||||
)(params)
|
||||
env.CACHE_TIME_HOTELS
|
||||
)
|
||||
}
|
||||
|
||||
export async function getHotelIdsByCountry(
|
||||
country: string,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (params: URLSearchParams) {
|
||||
export async function getHotelIdsByCountry({
|
||||
country,
|
||||
serviceToken,
|
||||
}: {
|
||||
country: string
|
||||
serviceToken: string
|
||||
}) {
|
||||
const cacheClient = await getCacheClient()
|
||||
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${country}:hotelsByCountry`,
|
||||
async () => {
|
||||
metrics.hotelIds.counter.add(1, { country })
|
||||
console.info(
|
||||
"api.hotel.hotel-ids start",
|
||||
JSON.stringify({ query: { country } })
|
||||
)
|
||||
|
||||
const hotelIdsParams = new URLSearchParams({
|
||||
country,
|
||||
})
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.hotels,
|
||||
options,
|
||||
params
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
hotelIdsParams
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
@@ -383,7 +430,7 @@ export async function getHotelIdsByCountry(
|
||||
})
|
||||
)
|
||||
|
||||
return []
|
||||
throw new Error("Unable to fetch hotelIds by country")
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
@@ -401,7 +448,7 @@ export async function getHotelIdsByCountry(
|
||||
error: validatedHotelIds.error,
|
||||
})
|
||||
)
|
||||
return []
|
||||
throw new Error("Unable to parse hotelIds by country")
|
||||
}
|
||||
|
||||
metrics.hotelIds.success.add(1, { country })
|
||||
@@ -412,62 +459,45 @@ export async function getHotelIdsByCountry(
|
||||
|
||||
return validatedHotelIds.data
|
||||
},
|
||||
[`hotelsByCountry`, params.toString()],
|
||||
{ revalidate: env.CACHE_TIME_HOTELS }
|
||||
)(params)
|
||||
env.CACHE_TIME_HOTELS
|
||||
)
|
||||
}
|
||||
|
||||
export async function getHotelIdsByCityIdentifier(
|
||||
cityIdentifier: string,
|
||||
serviceToken: string
|
||||
) {
|
||||
const apiLang = toApiLang(Lang.en)
|
||||
const city = await getCityByCityIdentifier(cityIdentifier, serviceToken)
|
||||
const city = await getCityByCityIdentifier({
|
||||
cityIdentifier,
|
||||
lang: Lang.en,
|
||||
serviceToken,
|
||||
})
|
||||
|
||||
if (!city) {
|
||||
return []
|
||||
}
|
||||
|
||||
const hotelIdsParams = new URLSearchParams({
|
||||
language: apiLang,
|
||||
city: city.id,
|
||||
const hotelIds = await getHotelIdsByCityId({
|
||||
cityId: city.id,
|
||||
serviceToken,
|
||||
})
|
||||
const options: RequestOptionsWithOutBody = {
|
||||
// needs to clear default option as only
|
||||
// cache or next.revalidate is permitted
|
||||
cache: undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: env.CACHE_TIME_HOTELS,
|
||||
},
|
||||
}
|
||||
const hotelIds = await getHotelIdsByCityId(city.id, options, hotelIdsParams)
|
||||
return hotelIds
|
||||
}
|
||||
|
||||
export async function getCityByCityIdentifier(
|
||||
cityIdentifier: string,
|
||||
export async function getCityByCityIdentifier({
|
||||
cityIdentifier,
|
||||
lang,
|
||||
serviceToken,
|
||||
}: {
|
||||
cityIdentifier: string
|
||||
lang: Lang
|
||||
serviceToken: string
|
||||
) {
|
||||
const lang = Lang.en
|
||||
const apiLang = toApiLang(lang)
|
||||
const options: RequestOptionsWithOutBody = {
|
||||
// needs to clear default option as only
|
||||
// cache or next.revalidate is permitted
|
||||
cache: undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: env.CACHE_TIME_HOTELS,
|
||||
},
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
language: apiLang,
|
||||
}) {
|
||||
const locations = await getLocations({
|
||||
lang,
|
||||
citiesByCountry: null,
|
||||
serviceToken,
|
||||
})
|
||||
const locations = await getLocations(lang, options, params, null)
|
||||
if (!locations || "error" in locations) {
|
||||
return null
|
||||
}
|
||||
@@ -479,11 +509,15 @@ export async function getCityByCityIdentifier(
|
||||
return city ?? null
|
||||
}
|
||||
|
||||
export async function getHotelsByHotelIds(
|
||||
hotelIds: string[],
|
||||
lang: Lang,
|
||||
export async function getHotelsByHotelIds({
|
||||
hotelIds,
|
||||
lang,
|
||||
serviceToken,
|
||||
}: {
|
||||
hotelIds: string[]
|
||||
lang: Lang
|
||||
serviceToken: string
|
||||
) {
|
||||
}) {
|
||||
const hotelPages = await getHotelPageUrls(lang)
|
||||
const hotels = await Promise.all(
|
||||
hotelIds.map(async (hotelId) => {
|
||||
|
||||
Reference in New Issue
Block a user