Merged in feature/SW-3572-hotel-data-endpoint (pull request #3051)
SW-3572 API route for listing hotels per city or country * wip hotel data endpoint * Correct route params type * wip * skip static paths call * timeout when getting destinations take too long * call noStore when we get a timeout * add cache-control headers * . * . * . * wip * wip * wip * wip * add route for getting hotels per country * include city when listing by country * fix distance SI unit * fix sorting * Merge branch 'master' of bitbucket.org:scandic-swap/web into feature/SW-3572-hotel-data-endpoint * packages/tracking passWithNoTests * revalidate must be static value * remove oxc reference * cleanup * cleanup hotel api route * feat(SW-3572): cleanup error handling Approved-by: Anton Gunnarsson
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
import deepmerge from "deepmerge"
|
||||
import { z } from "zod"
|
||||
|
||||
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||
import { chunk } from "@scandic-hotels/common/utils/chunk"
|
||||
|
||||
import * as api from "../../../api"
|
||||
import { serverErrorByStatus } from "../../../errors"
|
||||
import { toApiLang } from "../../../utils"
|
||||
import { locationCitySchema } from "../schemas/location/city"
|
||||
import { locationHotelSchema } from "../schemas/location/hotel"
|
||||
import { getCity } from "./getCity"
|
||||
|
||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||
|
||||
import type { Country } from "../../../types/country"
|
||||
|
||||
const hotelUtilsLogger = createLogger("getLocationsByCountries")
|
||||
|
||||
type CitiesNamesByCountry = Record<
|
||||
Country | (string & {}),
|
||||
Array<{ name: string }>
|
||||
> | null
|
||||
|
||||
export async function getLocationsByCountries({
|
||||
lang,
|
||||
citiesByCountry,
|
||||
serviceToken,
|
||||
}: {
|
||||
lang: Lang
|
||||
citiesByCountry: CitiesNamesByCountry | null
|
||||
serviceToken: string
|
||||
}) {
|
||||
const cacheClient = await getCacheClient()
|
||||
const countryKeys = Object.keys(citiesByCountry ?? {})
|
||||
let cacheKey = `${lang}:locations`
|
||||
|
||||
if (countryKeys.length > 0) {
|
||||
cacheKey += `:${countryKeys.toSorted().join(",")}`
|
||||
}
|
||||
|
||||
return await cacheClient.cacheOrGet(
|
||||
cacheKey.toLowerCase(),
|
||||
async () => {
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.locations,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
language: toApiLang(lang),
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
throw serverErrorByStatus(apiResponse.status, { apiResponse })
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedLocations = locationsSchema.safeParse(apiJson)
|
||||
if (!verifiedLocations.success) {
|
||||
hotelUtilsLogger.error(
|
||||
`Locations Verification Failed`,
|
||||
verifiedLocations.error
|
||||
)
|
||||
throw new Error("Unable to parse api response for locations", {
|
||||
cause: verifiedLocations.error,
|
||||
})
|
||||
}
|
||||
|
||||
const data = cleanData(verifiedLocations.data.data)
|
||||
const cities = data
|
||||
.filter((x) => x.type === "cities")
|
||||
.map((x) => enrichCity(x, citiesByCountry))
|
||||
|
||||
const chunkedHotels = chunk(
|
||||
data.filter((x) => x.type === "hotels"),
|
||||
10
|
||||
)
|
||||
const hotels = (
|
||||
await Promise.all(
|
||||
chunkedHotels.flatMap(async (chunk) => {
|
||||
return await Promise.all(
|
||||
chunk.flatMap(async (hotel) => enrichHotel(hotel, serviceToken))
|
||||
)
|
||||
})
|
||||
)
|
||||
).flat()
|
||||
|
||||
let locations: z.infer<typeof locationsSchema>["data"] = [
|
||||
...cities,
|
||||
...hotels,
|
||||
]
|
||||
|
||||
return locations
|
||||
},
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
async function enrichHotel(
|
||||
hotel: Extract<
|
||||
z.infer<typeof locationsSchema>["data"][number],
|
||||
{ type: "hotels" }
|
||||
>,
|
||||
serviceToken: string
|
||||
): Promise<
|
||||
Extract<z.infer<typeof locationsSchema>["data"][number], { type: "hotels" }>
|
||||
> {
|
||||
if (hotel.type !== "hotels") {
|
||||
return hotel
|
||||
}
|
||||
|
||||
if (!hotel.relationships.city?.url) {
|
||||
return hotel
|
||||
}
|
||||
const city = await getCity({
|
||||
cityUrl: hotel.relationships.city.url,
|
||||
serviceToken,
|
||||
})
|
||||
|
||||
if (!city) {
|
||||
return hotel
|
||||
}
|
||||
|
||||
return deepmerge(hotel, {
|
||||
relationships: {
|
||||
city,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function enrichCity(
|
||||
city: Extract<
|
||||
z.infer<typeof locationsSchema>["data"][number],
|
||||
{ type: "cities" }
|
||||
>,
|
||||
citiesByCountry: CitiesNamesByCountry | null
|
||||
): Extract<
|
||||
z.infer<typeof locationsSchema>["data"][number],
|
||||
{ type: "cities" }
|
||||
> {
|
||||
if (!citiesByCountry) {
|
||||
return city
|
||||
}
|
||||
|
||||
const country = Object.keys(citiesByCountry).find((country) =>
|
||||
citiesByCountry[country].find((loc) => loc.name === city.name)
|
||||
)
|
||||
if (!country) {
|
||||
hotelUtilsLogger.error(
|
||||
`Location cannot be found in any of the countries cities`,
|
||||
city
|
||||
)
|
||||
|
||||
return city
|
||||
}
|
||||
|
||||
return {
|
||||
...city,
|
||||
country,
|
||||
}
|
||||
}
|
||||
|
||||
function cleanData(data: z.infer<typeof locationsSchema>["data"]) {
|
||||
return data
|
||||
.filter((node) => {
|
||||
if (node?.isPublished !== true) {
|
||||
return false
|
||||
}
|
||||
if (node.type === "hotels" && !node.operaId) {
|
||||
return false
|
||||
}
|
||||
if (node.type === "cities" && !node.cityIdentifier) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
.toSorted((a, b) => {
|
||||
if (a.type === b.type) {
|
||||
return a.name.localeCompare(b.name)
|
||||
} else {
|
||||
return a.type === "cities" ? -1 : 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const locationsSchema = z.object({
|
||||
data: z.array(
|
||||
z
|
||||
.discriminatedUnion("type", [locationCitySchema, locationHotelSchema])
|
||||
.transform((location) => {
|
||||
if (location.type === "cities") {
|
||||
return {
|
||||
...location.attributes,
|
||||
country: location.attributes.countryName || "",
|
||||
id: location.id,
|
||||
type: location.type,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...location.attributes,
|
||||
id: location.id,
|
||||
relationships: {
|
||||
city: {
|
||||
cityIdentifier: "",
|
||||
ianaTimeZoneId: "",
|
||||
id: "",
|
||||
isPublished: false,
|
||||
keywords: [],
|
||||
name: "",
|
||||
timeZoneId: "",
|
||||
type: "cities",
|
||||
url: location?.relationships?.city?.links?.related ?? "",
|
||||
},
|
||||
},
|
||||
type: location.type,
|
||||
operaId: location.attributes.operaId ?? "",
|
||||
}
|
||||
})
|
||||
),
|
||||
})
|
||||
Reference in New Issue
Block a user