Merge branch 'develop' into feature/tracking
This commit is contained in:
@@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import * as api from "@/lib/api"
|
||||
import { getVerifiedUser } from "@/server/routers/user/query"
|
||||
import { bookingServiceProcedure, router } from "@/server/trpc"
|
||||
import { router, serviceProcedure } from "@/server/trpc"
|
||||
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
@@ -36,7 +36,7 @@ async function getMembershipNumber(
|
||||
|
||||
export const bookingMutationRouter = router({
|
||||
booking: router({
|
||||
create: bookingServiceProcedure
|
||||
create: serviceProcedure
|
||||
.input(createBookingInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const { checkInDate, checkOutDate, hotelId } = input
|
||||
|
||||
@@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import * as api from "@/lib/api"
|
||||
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
|
||||
import { bookingServiceProcedure, router } from "@/server/trpc"
|
||||
import { router, serviceProcedure } from "@/server/trpc"
|
||||
|
||||
import { getBookingStatusInput } from "./input"
|
||||
import { createBookingSchema } from "./output"
|
||||
@@ -17,69 +17,70 @@ const getBookingStatusFailCounter = meter.createCounter(
|
||||
)
|
||||
|
||||
export const bookingQueryRouter = router({
|
||||
status: bookingServiceProcedure
|
||||
.input(getBookingStatusInput)
|
||||
.query(async function ({ ctx, input }) {
|
||||
const { confirmationNumber } = input
|
||||
getBookingStatusCounter.add(1, { confirmationNumber })
|
||||
status: serviceProcedure.input(getBookingStatusInput).query(async function ({
|
||||
ctx,
|
||||
input,
|
||||
}) {
|
||||
const { confirmationNumber } = input
|
||||
getBookingStatusCounter.add(1, { confirmationNumber })
|
||||
|
||||
const apiResponse = await api.get(
|
||||
`${api.endpoints.v1.booking}/${confirmationNumber}/status`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const responseMessage = await apiResponse.text()
|
||||
getBookingStatusFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "http_error",
|
||||
error: responseMessage,
|
||||
})
|
||||
console.error(
|
||||
"api.booking.status error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text: responseMessage,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
const apiResponse = await api.get(
|
||||
`${api.endpoints.v1.booking}/${confirmationNumber}/status`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
getBookingStatusFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(verifiedData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.status validation error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
getBookingStatusSuccessCounter.add(1, { confirmationNumber })
|
||||
console.info(
|
||||
"api.booking.status success",
|
||||
if (!apiResponse.ok) {
|
||||
const responseMessage = await apiResponse.text()
|
||||
getBookingStatusFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "http_error",
|
||||
error: responseMessage,
|
||||
})
|
||||
console.error(
|
||||
"api.booking.status error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text: responseMessage,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
getBookingStatusFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(verifiedData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.status validation error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
getBookingStatusSuccessCounter.add(1, { confirmationNumber })
|
||||
console.info(
|
||||
"api.booking.status success",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
})
|
||||
)
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { unstable_cache } from "next/cache"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import * as api from "@/lib/api"
|
||||
import { GetRewards } from "@/lib/graphql/Query/Rewards.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { Context } from "@/server/context"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import {
|
||||
contentStackBaseWithProfileServiceProcedure,
|
||||
contentStackBaseWithProtectedProcedure,
|
||||
contentStackBaseWithServiceProcedure,
|
||||
router,
|
||||
} from "@/server/trpc"
|
||||
|
||||
@@ -62,69 +62,71 @@ const getAllRewardFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.all-fail"
|
||||
)
|
||||
|
||||
const ONE_HOUR = 60 * 60
|
||||
|
||||
function getUniqueRewardIds(rewardIds: string[]) {
|
||||
const uniqueRewardIds = new Set(rewardIds)
|
||||
return Array.from(uniqueRewardIds)
|
||||
}
|
||||
|
||||
async function getAllApiRewards(ctx: Context & { serviceToken: string }) {
|
||||
const apiResponse = await api.get(api.endpoints.v1.tierRewards, {
|
||||
cache: undefined, // override defaultOptions
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
// One hour. Since the service token is refreshed every hour, this is the longest cache we can have.
|
||||
next: { revalidate: 60 * 60 },
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
const getAllCachedApiRewards = unstable_cache(
|
||||
async function (token) {
|
||||
const apiResponse = await api.get(api.endpoints.v1.tierRewards, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
console.error(
|
||||
"api.rewards.tierRewards error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
console.error(
|
||||
"api.rewards.tierRewards error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data)
|
||||
const data = await apiResponse.json()
|
||||
const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data)
|
||||
|
||||
if (!validatedApiTierRewards.success) {
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiTierRewards.error),
|
||||
})
|
||||
console.error(validatedApiTierRewards.error)
|
||||
console.error(
|
||||
"api.rewards validation error",
|
||||
JSON.stringify({
|
||||
error: validatedApiTierRewards.error,
|
||||
if (!validatedApiTierRewards.success) {
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiTierRewards.error),
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
console.error(validatedApiTierRewards.error)
|
||||
console.error(
|
||||
"api.rewards validation error",
|
||||
JSON.stringify({
|
||||
error: validatedApiTierRewards.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return validatedApiTierRewards.data
|
||||
}
|
||||
return validatedApiTierRewards.data
|
||||
},
|
||||
["getAllApiRewards"],
|
||||
{ revalidate: ONE_HOUR }
|
||||
)
|
||||
|
||||
async function getCmsRewards(locale: Lang, rewardIds: string[]) {
|
||||
const tags = rewardIds.map((id) =>
|
||||
generateLoyaltyConfigTag(locale, "reward", id)
|
||||
)
|
||||
|
||||
const cmsRewardsResponse = await request<CmsRewardsResponse>(
|
||||
GetRewards,
|
||||
{
|
||||
@@ -260,13 +262,15 @@ export const rewardQueryRouter = router({
|
||||
nextCursor,
|
||||
}
|
||||
}),
|
||||
byLevel: contentStackBaseWithProfileServiceProcedure
|
||||
byLevel: contentStackBaseWithServiceProcedure
|
||||
.input(rewardsByLevelInput)
|
||||
.query(async function ({ input, ctx }) {
|
||||
getByLevelRewardCounter.add(1)
|
||||
const { level_id } = input
|
||||
|
||||
const allUpcomingApiRewards = await getAllApiRewards(ctx)
|
||||
const allUpcomingApiRewards = await getAllCachedApiRewards(
|
||||
ctx.serviceToken
|
||||
)
|
||||
|
||||
if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) {
|
||||
getByLevelRewardFailCounter.add(1)
|
||||
@@ -310,11 +314,11 @@ export const rewardQueryRouter = router({
|
||||
getByLevelRewardSuccessCounter.add(1)
|
||||
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
|
||||
}),
|
||||
all: contentStackBaseWithProfileServiceProcedure
|
||||
all: contentStackBaseWithServiceProcedure
|
||||
.input(rewardsAllInput)
|
||||
.query(async function ({ input, ctx }) {
|
||||
getAllRewardCounter.add(1)
|
||||
const allApiRewards = await getAllApiRewards(ctx)
|
||||
const allApiRewards = await getAllCachedApiRewards(ctx.serviceToken)
|
||||
|
||||
if (!allApiRewards) {
|
||||
return []
|
||||
|
||||
@@ -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"
|
||||
@@ -11,10 +10,10 @@ import {
|
||||
} from "@/server/errors/trpc"
|
||||
import { extractHotelImages } from "@/server/routers/utils/hotels"
|
||||
import {
|
||||
contentStackUidWithHotelServiceProcedure,
|
||||
hotelServiceProcedure,
|
||||
contentStackUidWithServiceProcedure,
|
||||
publicProcedure,
|
||||
router,
|
||||
serviceProcedure,
|
||||
} from "@/server/trpc"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
@@ -38,7 +37,6 @@ import {
|
||||
getCitiesByCountry,
|
||||
getCountries,
|
||||
getLocations,
|
||||
locationsAffix,
|
||||
TWENTYFOUR_HOURS,
|
||||
} from "./utils"
|
||||
|
||||
@@ -99,7 +97,7 @@ async function getContentstackData(
|
||||
}
|
||||
|
||||
export const hotelQueryRouter = router({
|
||||
get: contentStackUidWithHotelServiceProcedure
|
||||
get: contentStackUidWithServiceProcedure
|
||||
.input(getHotelInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { lang, uid } = ctx
|
||||
@@ -264,7 +262,7 @@ export const hotelQueryRouter = router({
|
||||
}
|
||||
}),
|
||||
availability: router({
|
||||
hotels: hotelServiceProcedure
|
||||
hotels: serviceProcedure
|
||||
.input(getHotelsAvailabilityInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const {
|
||||
@@ -388,7 +386,7 @@ export const hotelQueryRouter = router({
|
||||
.flatMap((hotels) => hotels.attributes),
|
||||
}
|
||||
}),
|
||||
rooms: hotelServiceProcedure
|
||||
rooms: serviceProcedure
|
||||
.input(getRoomsAvailabilityInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const {
|
||||
@@ -543,7 +541,7 @@ export const hotelQueryRouter = router({
|
||||
}),
|
||||
}),
|
||||
hotelData: router({
|
||||
get: hotelServiceProcedure
|
||||
get: serviceProcedure
|
||||
.input(getlHotelDataInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { hotelId, language, include } = input
|
||||
@@ -641,7 +639,7 @@ export const hotelQueryRouter = router({
|
||||
}),
|
||||
}),
|
||||
locations: router({
|
||||
get: hotelServiceProcedure.query(async function ({ ctx }) {
|
||||
get: serviceProcedure.query(async function ({ ctx }) {
|
||||
const searchParams = new URLSearchParams()
|
||||
searchParams.set("language", toApiLang(ctx.lang))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { SafeParseSuccess } from "zod"
|
||||
|
||||
import * as api from "@/lib/api"
|
||||
import {
|
||||
|
||||
@@ -1,35 +1,85 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { revalidateTag, unstable_cache } from "next/cache"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import { generateServiceTokenTag } from "@/utils/generateTag"
|
||||
|
||||
import { ServiceTokenResponse } from "@/types/tokens"
|
||||
|
||||
const SERVICE_TOKEN_REVALIDATE_SECONDS = 3599 // 59 minutes and 59 seconds.
|
||||
// OpenTelemetry metrics: Service token
|
||||
const meter = metrics.getMeter("trpc.context.serviceToken")
|
||||
const fetchServiceTokenCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.fetch-new-token"
|
||||
)
|
||||
const fetchTempServiceTokenCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.fetch-temporary"
|
||||
)
|
||||
const fetchServiceTokenFailCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.fetch-fail"
|
||||
)
|
||||
|
||||
export async function fetchServiceToken(
|
||||
scopes: string[]
|
||||
): Promise<ServiceTokenResponse> {
|
||||
try {
|
||||
const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
client_id: env.CURITY_CLIENT_ID_SERVICE,
|
||||
client_secret: env.CURITY_CLIENT_SECRET_SERVICE,
|
||||
scope: scopes.join(" "),
|
||||
async function fetchServiceToken(scopes: string[]) {
|
||||
fetchServiceTokenCounter.add(1)
|
||||
const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
client_id: env.CURITY_CLIENT_ID_SERVICE,
|
||||
client_secret: env.CURITY_CLIENT_SECRET_SERVICE,
|
||||
scope: scopes.join(" "),
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
fetchServiceTokenFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
}),
|
||||
next: {
|
||||
revalidate: SERVICE_TOKEN_REVALIDATE_SECONDS,
|
||||
},
|
||||
})
|
||||
throw new Error("Failed to obtain service token")
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to obtain service token")
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function getServiceToken(): Promise<ServiceTokenResponse> {
|
||||
try {
|
||||
const scopes = ["profile", "hotel", "booking"]
|
||||
const tag = generateServiceTokenTag(scopes)
|
||||
const getCachedJwt = unstable_cache(
|
||||
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(scopes)
|
||||
if (cachedJwt.expiresAt < Date.now()) {
|
||||
console.log(
|
||||
"trpc.context.serviceToken: Service token expired, revalidating tag"
|
||||
)
|
||||
revalidateTag(tag)
|
||||
|
||||
console.log(
|
||||
"trpc.context.serviceToken: Fetching new temporary service token."
|
||||
)
|
||||
fetchTempServiceTokenCounter.add(1)
|
||||
const newToken = await fetchServiceToken(scopes)
|
||||
return newToken
|
||||
}
|
||||
|
||||
return response.json()
|
||||
return cachedJwt.jwt
|
||||
} catch (error) {
|
||||
console.error("Error fetching service token:", error)
|
||||
throw error
|
||||
|
||||
@@ -11,7 +11,7 @@ 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"
|
||||
|
||||
@@ -121,23 +121,17 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) {
|
||||
})
|
||||
})
|
||||
|
||||
function createServiceProcedure(serviceName: string) {
|
||||
return t.procedure.use(async (opts) => {
|
||||
const { access_token } = await fetchServiceToken([serviceName])
|
||||
if (!access_token) {
|
||||
throw internalServerError(`Failed to obtain ${serviceName} service token`)
|
||||
}
|
||||
return opts.next({
|
||||
ctx: {
|
||||
serviceToken: access_token,
|
||||
},
|
||||
})
|
||||
export const serviceProcedure = t.procedure.use(async (opts) => {
|
||||
const { access_token } = await getServiceToken()
|
||||
if (!access_token) {
|
||||
throw internalServerError(`Failed to obtain service token`)
|
||||
}
|
||||
return opts.next({
|
||||
ctx: {
|
||||
serviceToken: access_token,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const bookingServiceProcedure = createServiceProcedure("booking")
|
||||
export const hotelServiceProcedure = createServiceProcedure("hotel")
|
||||
export const profileServiceProcedure = createServiceProcedure("profile")
|
||||
})
|
||||
|
||||
export const serverActionProcedure = t.procedure.experimental_caller(
|
||||
experimental_nextAppDirCaller({
|
||||
@@ -168,11 +162,11 @@ export const protectedServerActionProcedure = serverActionProcedure.use(
|
||||
|
||||
// NOTE: This is actually save to use, just the implementation could change
|
||||
// in minor version bumps. Please read: https://trpc.io/docs/faq#unstable
|
||||
export const contentStackUidWithHotelServiceProcedure =
|
||||
contentstackExtendedProcedureUID.unstable_concat(hotelServiceProcedure)
|
||||
export const contentStackUidWithServiceProcedure =
|
||||
contentstackExtendedProcedureUID.unstable_concat(serviceProcedure)
|
||||
|
||||
export const contentStackBaseWithProfileServiceProcedure =
|
||||
contentstackBaseProcedure.unstable_concat(profileServiceProcedure)
|
||||
export const contentStackBaseWithServiceProcedure =
|
||||
contentstackBaseProcedure.unstable_concat(serviceProcedure)
|
||||
|
||||
export const contentStackBaseWithProtectedProcedure =
|
||||
contentstackBaseProcedure.unstable_concat(protectedProcedure)
|
||||
|
||||
Reference in New Issue
Block a user