From ed3acce57c728ee952a93637a0104d9442e72199 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 9 Oct 2024 11:27:16 +0200 Subject: [PATCH] fix: add args as keyParts for unstable_cache --- server/routers/hotels/query.ts | 29 +-- server/routers/hotels/utils.ts | 300 +++++++++++++++----------- server/tokenManager.ts | 37 ++-- server/trpc.ts | 8 +- types/enums/serviceToken.ts | 7 - types/trpc/routers/hotel/locations.ts | 3 + utils/generateTag.ts | 6 +- 7 files changed, 204 insertions(+), 186 deletions(-) delete mode 100644 types/enums/serviceToken.ts diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 8ce752577..f0c5e1ef8 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -1,5 +1,4 @@ import { metrics } from "@opentelemetry/api" -import { unstable_cache } from "next/cache" import * as api from "@/lib/api" import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql" @@ -38,7 +37,6 @@ import { getCitiesByCountry, getCountries, getLocations, - locationsAffix, TWENTYFOUR_HOURS, } from "./utils" @@ -657,36 +655,19 @@ export const hotelQueryRouter = router({ }, } - const getCachedCountries = unstable_cache( - getCountries, - [`${ctx.lang}:${locationsAffix}:countries`], - { revalidate: TWENTYFOUR_HOURS } - ) - - const countries = await getCachedCountries(options, searchParams) - - const getCachedCitiesByCountry = unstable_cache( - getCitiesByCountry, - [`${ctx.lang}:${locationsAffix}:cities-by-country`], - { revalidate: TWENTYFOUR_HOURS } - ) + const countries = await getCountries(options, searchParams, ctx.lang) let citiesByCountry = null if (countries) { - citiesByCountry = await getCachedCitiesByCountry( + citiesByCountry = await getCitiesByCountry( countries, options, - searchParams + searchParams, + ctx.lang ) } - const getCachedLocations = unstable_cache( - getLocations, - [`${ctx.lang}:${locationsAffix}`], - { revalidate: TWENTYFOUR_HOURS } - ) - - const locations = await getCachedLocations( + const locations = await getLocations( ctx.lang, options, searchParams, diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts index d19d1a492..84020bfaa 100644 --- a/server/routers/hotels/utils.ts +++ b/server/routers/hotels/utils.ts @@ -17,6 +17,7 @@ import { PointOfInterestCategoryNameEnum, PointOfInterestGroupEnum, } from "@/types/hotel" +import { HotelLocation } from "@/types/trpc/routers/hotel/locations" import type { Lang } from "@/constants/languages" import type { Endpoint } from "@/lib/api/endpoints" @@ -54,89 +55,119 @@ export const locationsAffix = "locations" export const TWENTYFOUR_HOURS = 60 * 60 * 24 export async function getCity( cityUrl: string, - options: RequestOptionsWithOutBody + options: RequestOptionsWithOutBody, + lang: Lang, + relationshipCity: HotelLocation["relationships"]["city"] ) { - const url = new URL(cityUrl) - const cityResponse = await api.get( - url.pathname as Endpoint, - options, - url.searchParams - ) + return unstable_cache( + async function (locationCityUrl: string) { + const url = new URL(locationCityUrl) + const cityResponse = await api.get( + url.pathname as Endpoint, + options, + url.searchParams + ) - if (!cityResponse.ok) { - return null - } + if (!cityResponse.ok) { + return null + } - const cityJson = await cityResponse.json() - const city = apiCitySchema.safeParse(cityJson) - if (!city.success) { - console.info(`Validation of city failed`) - console.info(`cityUrl: ${cityUrl}`) - console.error(city.error) - return null - } + const cityJson = await cityResponse.json() + const city = apiCitySchema.safeParse(cityJson) + if (!city.success) { + console.info(`Validation of city failed`) + console.info(`cityUrl: ${locationCityUrl}`) + console.error(city.error) + return null + } - return city.data + return city.data + }, + [cityUrl, `${lang}:${relationshipCity}`], + { revalidate: TWENTYFOUR_HOURS } + )(cityUrl) } export async function getCountries( options: RequestOptionsWithOutBody, - params: URLSearchParams + params: URLSearchParams, + lang: Lang ) { - const countryResponse = await api.get( - api.endpoints.v1.countries, - options, - params - ) - - if (!countryResponse.ok) { - return null - } - - const countriesJson = await countryResponse.json() - const countries = apiCountriesSchema.safeParse(countriesJson) - if (!countries.success) { - console.info(`Validation for countries failed`) - console.error(countries.error) - return null - } - - return countries.data -} - -export async function getCitiesByCountry( - countries: Countries, - options: RequestOptionsWithOutBody, - params: URLSearchParams -) { - const citiesGroupedByCountry: CitiesGroupedByCountry = {} - - await Promise.all( - countries.data.map(async (country) => { + return unstable_cache( + async function (searchParams) { const countryResponse = await api.get( - `${api.endpoints.v1.citiesCountry}/${country.name}`, + api.endpoints.v1.countries, options, - params + searchParams ) if (!countryResponse.ok) { return null } - const countryJson = await countryResponse.json() - const citiesByCountry = apiCitiesByCountrySchema.safeParse(countryJson) - if (!citiesByCountry.success) { - console.info(`Failed to validate Cities by Country payload`) - console.error(citiesByCountry.error) + const countriesJson = await countryResponse.json() + const countries = apiCountriesSchema.safeParse(countriesJson) + if (!countries.success) { + console.info(`Validation for countries failed`) + console.error(countries.error) return null } - citiesGroupedByCountry[country.name] = citiesByCountry.data.data - return true - }) - ) + return countries.data + }, + [`${lang}:${locationsAffix}:countries`, params.toString()], + { revalidate: TWENTYFOUR_HOURS } + )(params) +} - return citiesGroupedByCountry +export async function getCitiesByCountry( + countries: Countries, + options: RequestOptionsWithOutBody, + params: URLSearchParams, + lang: Lang +) { + return unstable_cache( + async function ( + searchParams: URLSearchParams, + searchedCountries: Countries + ) { + const citiesGroupedByCountry: CitiesGroupedByCountry = {} + + await Promise.all( + searchedCountries.data.map(async (country) => { + const countryResponse = await api.get( + `${api.endpoints.v1.citiesCountry}/${country.name}`, + options, + searchParams + ) + + if (!countryResponse.ok) { + return null + } + + const countryJson = await countryResponse.json() + const citiesByCountry = + apiCitiesByCountrySchema.safeParse(countryJson) + if (!citiesByCountry.success) { + console.info(`Failed to validate Cities by Country payload`) + console.error(citiesByCountry.error) + return null + } + + citiesGroupedByCountry[country.name] = citiesByCountry.data.data + return true + }) + ) + + return citiesGroupedByCountry + }, + [ + `${lang}:${locationsAffix}:cities-by-country`, + params.toString(), + JSON.stringify(countries), + ], + { revalidate: TWENTYFOUR_HOURS } + )(params, countries) } export async function getLocations( @@ -145,72 +176,89 @@ export async function getLocations( params: URLSearchParams, citiesByCountry: CitiesGroupedByCountry | null ) { - const apiResponse = await api.get(api.endpoints.v1.locations, options, params) + return unstable_cache( + async function ( + searchParams: URLSearchParams, + groupedCitiesByCountry: CitiesGroupedByCountry | null + ) { + const apiResponse = await api.get( + api.endpoints.v1.locations, + options, + searchParams + ) - if (!apiResponse.ok) { - if (apiResponse.status === 401) { - return { error: true, cause: "unauthorized" } as const - } else if (apiResponse.status === 403) { - return { error: true, cause: "forbidden" } as const - } - return null - } - - const apiJson = await apiResponse.json() - const verifiedLocations = apiLocationsSchema.safeParse(apiJson) - if (!verifiedLocations.success) { - console.info(`Locations Verification Failed`) - console.error(verifiedLocations.error) - return null - } - - return await Promise.all( - verifiedLocations.data.data.map(async (location) => { - if (location.type === "cities") { - if (citiesByCountry) { - const country = Object.keys(citiesByCountry).find((country) => { - if ( - citiesByCountry[country].find((loc) => loc.name === location.name) - ) { - return true - } - return false - }) - if (country) { - return { - ...location, - country, - } - } else { - console.info( - `Location cannot be found in any of the countries cities` - ) - console.info(location) - } - } - } else if (location.type === "hotels") { - if (location.relationships.city?.url) { - const getCachedCity = unstable_cache( - getCity, - [`${lang}:${location.relationships.city}`], - { revalidate: TWENTYFOUR_HOURS } - ) - - const city = await getCachedCity( - location.relationships.city.url, - options - ) - if (city) { - return deepmerge(location, { - relationships: { - city, - }, - }) - } + if (!apiResponse.ok) { + if (apiResponse.status === 401) { + return { error: true, cause: "unauthorized" } as const + } else if (apiResponse.status === 403) { + return { error: true, cause: "forbidden" } as const } + return null } - return location - }) - ) + const apiJson = await apiResponse.json() + const verifiedLocations = apiLocationsSchema.safeParse(apiJson) + if (!verifiedLocations.success) { + console.info(`Locations Verification Failed`) + console.error(verifiedLocations.error) + return null + } + + 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 (country) { + return { + ...location, + country, + } + } else { + console.info( + `Location cannot be found in any of the countries cities` + ) + console.info(location) + } + } + } else if (location.type === "hotels") { + if (location.relationships.city?.url) { + const city = await getCity( + location.relationships.city.url, + options, + lang, + location.relationships.city + ) + if (city) { + return deepmerge(location, { + relationships: { + city, + }, + }) + } + } + } + + return location + }) + ) + }, + [ + `${lang}:${locationsAffix}`, + params.toString(), + JSON.stringify(citiesByCountry), + ], + { revalidate: TWENTYFOUR_HOURS } + )(params, citiesByCountry) } diff --git a/server/tokenManager.ts b/server/tokenManager.ts index 6df7def66..2e6d82b04 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -5,24 +5,22 @@ import { env } from "@/env/server" import { generateServiceTokenTag } from "@/utils/generateTag" -import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken" import { ServiceTokenResponse } from "@/types/tokens" // OpenTelemetry metrics: Service token const meter = metrics.getMeter("trpc.context.serviceToken") -const getServiceTokenCounter = meter.createCounter( - "trpc.context.serviceToken.get-new-token" +const fetchServiceTokenCounter = meter.createCounter( + "trpc.context.serviceToken.fetch-new-token" ) -const getTempServiceTokenCounter = meter.createCounter( - "trpc.context.serviceToken.get-temporary" +const fetchTempServiceTokenCounter = meter.createCounter( + "trpc.context.serviceToken.fetch-temporary" ) -const getServiceTokenFailCounter = meter.createCounter( - "trpc.context.serviceToken.get-fail" +const fetchServiceTokenFailCounter = meter.createCounter( + "trpc.context.serviceToken.fetch-fail" ) -async function getServiceToken() { - getServiceTokenCounter.add(1) - const scopes = Object.keys(ServiceTokenScopeEnum) +async function fetchServiceToken(scopes: string[]) { + fetchServiceTokenCounter.add(1) const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, { method: "POST", headers: { @@ -38,7 +36,7 @@ async function getServiceToken() { }) if (!response.ok) { - getServiceTokenFailCounter.add(1, { + fetchServiceTokenFailCounter.add(1, { error_type: "http_error", error: JSON.stringify({ status: response.status, @@ -51,21 +49,22 @@ async function getServiceToken() { return response.json() } -export async function fetchServiceToken(): Promise { +export async function getServiceToken(): Promise { try { - const tag = generateServiceTokenTag() + const scopes = ["profile", "hotel", "booking"] + const tag = generateServiceTokenTag(scopes) const getCachedJwt = unstable_cache( - async () => { - const jwt = await getServiceToken() + async (scopes) => { + const jwt = await fetchServiceToken(scopes) const expiresAt = Date.now() + jwt.expires_in * 1000 return { expiresAt, jwt } }, - [], + [tag], { tags: [tag] } ) - const cachedJwt = await getCachedJwt() + const cachedJwt = await getCachedJwt(scopes) if (cachedJwt.expiresAt < Date.now()) { console.log( "trpc.context.serviceToken: Service token expired, revalidating tag" @@ -75,8 +74,8 @@ export async function fetchServiceToken(): Promise { console.log( "trpc.context.serviceToken: Fetching new temporary service token." ) - getTempServiceTokenCounter.add(1) - const newToken = await getServiceToken() + fetchTempServiceTokenCounter.add(1) + const newToken = await fetchServiceToken(scopes) return newToken } diff --git a/server/trpc.ts b/server/trpc.ts index 94d4e47b5..dafd26690 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -11,16 +11,12 @@ import { unauthorizedError, } from "./errors/trpc" import { type Context, createContext } from "./context" -import { fetchServiceToken } from "./tokenManager" +import { getServiceToken } from "./tokenManager" import { transformer } from "./transformer" import { langInput } from "./utils" import type { Session } from "next-auth" -import { - ServiceTokenScope, - ServiceTokenScopeEnum, -} from "@/types/enums/serviceToken" import type { Meta } from "@/types/trpc/meta" const t = initTRPC @@ -126,7 +122,7 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) { }) export const serviceProcedure = t.procedure.use(async (opts) => { - const { access_token } = await fetchServiceToken() + const { access_token } = await getServiceToken() if (!access_token) { throw internalServerError(`Failed to obtain service token`) } diff --git a/types/enums/serviceToken.ts b/types/enums/serviceToken.ts deleted file mode 100644 index 4efbe1218..000000000 --- a/types/enums/serviceToken.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum ServiceTokenScopeEnum { - profile = "profile", - hotel = "hotel", - booking = "booking", -} - -export type ServiceTokenScope = keyof typeof ServiceTokenScopeEnum diff --git a/types/trpc/routers/hotel/locations.ts b/types/trpc/routers/hotel/locations.ts index 859aa8a03..8f2deed1e 100644 --- a/types/trpc/routers/hotel/locations.ts +++ b/types/trpc/routers/hotel/locations.ts @@ -6,3 +6,6 @@ export interface LocationSchema extends z.output {} export type Locations = LocationSchema["data"] export type Location = Locations[number] + +export type CityLocation = Location & { type: "cities" } +export type HotelLocation = Location & { type: "hotels" } diff --git a/utils/generateTag.ts b/utils/generateTag.ts index 0dbf95d33..b067f536e 100644 --- a/utils/generateTag.ts +++ b/utils/generateTag.ts @@ -1,4 +1,3 @@ -import { ServiceTokenScopeEnum } from "@/types/enums/serviceToken" import { System } from "@/types/requests/system" import type { Edges } from "@/types/requests/utils/edges" import type { NodeRefs } from "@/types/requests/utils/refs" @@ -107,7 +106,6 @@ export function generateLoyaltyConfigTag( * @param serviceTokenScope scope of service token * @returns string */ -export function generateServiceTokenTag() { - const scopes = Object.keys(ServiceTokenScopeEnum).join("-") - return `service_token:${scopes}` +export function generateServiceTokenTag(scopes: string[]) { + return `service_token:${scopes.join("-")}` }