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
@@ -3,4 +3,4 @@ nodeLinker: node-modules
|
|||||||
packageExtensions:
|
packageExtensions:
|
||||||
eslint-config-next@*:
|
eslint-config-next@*:
|
||||||
dependencies:
|
dependencies:
|
||||||
next: '*'
|
next: "*"
|
||||||
|
|||||||
1
apps/redis-api/.dockerignore
Normal file
1
apps/redis-api/.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env.local
|
||||||
175
apps/redis-api/.gitignore
vendored
Normal file
175
apps/redis-api/.gitignore
vendored
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
npm-debug.log_
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
|
||||||
|
pids
|
||||||
|
_.pid
|
||||||
|
_.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
21
apps/redis-api/Dockerfile
Normal file
21
apps/redis-api/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Use the official Bun image
|
||||||
|
FROM oven/bun:latest
|
||||||
|
ENV CI=true
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN bun install --production
|
||||||
|
|
||||||
|
# Copy the rest of the application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Expose the port the app runs on
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start the Bun server
|
||||||
|
CMD ["bun", "./src/index.ts"]
|
||||||
39
apps/redis-api/README.md
Normal file
39
apps/redis-api/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Redis API
|
||||||
|
|
||||||
|
A thin wrapper around redis so that we can communicate to it via HTTP instead of TCP
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Make sure you have access to Azure and have PIMed yourself to
|
||||||
|
|
||||||
|
- `Web-App-Frontend prod` where the ACR is located
|
||||||
|
- `Web Components Prod` or `Web Components Test` depending on where you want to deploy
|
||||||
|
|
||||||
|
Login with `az login` and select `Web-App-Frontend prod`
|
||||||
|
|
||||||
|
### Build container image
|
||||||
|
|
||||||
|
Standing in `/apps/redis-api` run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
az acr build . --image redis-api:latest -r acrscandicfrontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy container image
|
||||||
|
|
||||||
|
| Subscription | Environment | SubscriptionId |
|
||||||
|
| ------------------- | ----------- | ------------------------------------ |
|
||||||
|
| Web Components Prod | prod | 799cbffe-5209-41fd-adf9-4ffa3d1feead |
|
||||||
|
| Web Components Test | test | 3b657fc5-85b0-4a43-aba2-e77618ef98c4 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Replace with appropriate values
|
||||||
|
az deployment sub create \
|
||||||
|
--location westeurope \
|
||||||
|
--template-file ci/bicep/main.bicep \
|
||||||
|
--subscription {{SUBSCRIPTION_ID}} \
|
||||||
|
--parameters environment={{ENVIRONMENT}} \
|
||||||
|
containerImageTag=latest \
|
||||||
|
primaryApiKey={{PRIMARY API KEY}} \ # API keys are used for communicating with the api
|
||||||
|
secondaryApiKey={{SECONDARY API KEY}}
|
||||||
|
```
|
||||||
38
apps/redis-api/biome.jsonc
Normal file
38
apps/redis-api/biome.jsonc
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": false,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true,
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false,
|
||||||
|
"ignore": ["node_modules"],
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "tab",
|
||||||
|
},
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true,
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"performance": {
|
||||||
|
"noBarrelFile": "error",
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"useImportType": "error",
|
||||||
|
"useExportType": "error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "double",
|
||||||
|
"trailingCommas": "all",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
103
apps/redis-api/ci/azure-pipelines.build.yml
Normal file
103
apps/redis-api/ci/azure-pipelines.build.yml
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Docker
|
||||||
|
# Build a Docker image
|
||||||
|
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker
|
||||||
|
name: 1.0.0-$(SourceBranchName)-$(Rev:r)
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
- main
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
- name: forcePush
|
||||||
|
displayName: Force push
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- repo: self
|
||||||
|
variables:
|
||||||
|
tag: "$(Build.BuildNumber)"
|
||||||
|
imageName: "redis-api"
|
||||||
|
isMaster: $[eq(variables['Build.SourceBranchName'], 'master')]
|
||||||
|
shouldPush: $[or(eq(${{parameters.forcePush}}, True), eq(variables['isMaster'], True))]
|
||||||
|
tags: |
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- stage: Build
|
||||||
|
displayName: Set version
|
||||||
|
jobs:
|
||||||
|
- job: CreateArtifact
|
||||||
|
displayName: Create version artifact
|
||||||
|
steps:
|
||||||
|
- task: Bash@3
|
||||||
|
displayName: Write buildnumber
|
||||||
|
inputs:
|
||||||
|
targetType: "inline"
|
||||||
|
script: |
|
||||||
|
echo '$(Build.BuildNumber)' > $(Pipeline.Workspace)/.version
|
||||||
|
|
||||||
|
- task: PublishPipelineArtifact@1
|
||||||
|
inputs:
|
||||||
|
targetPath: "$(Pipeline.Workspace)/.version"
|
||||||
|
artifact: "Version"
|
||||||
|
publishLocation: "pipeline"
|
||||||
|
- task: Bash@3
|
||||||
|
displayName: Add tag main-latest if main branch
|
||||||
|
inputs:
|
||||||
|
targetType: "inline"
|
||||||
|
script: |
|
||||||
|
localTags = $(tag)
|
||||||
|
localTags += "\nlatest"
|
||||||
|
if [ $[isMaster] ]; then
|
||||||
|
localTags += "\nlatest-main"
|
||||||
|
echo -e "##vso[task.setvariable variable=tags;]$localTags"
|
||||||
|
fi
|
||||||
|
echo -e $localTags
|
||||||
|
|
||||||
|
- job: Build
|
||||||
|
displayName: Build
|
||||||
|
pool:
|
||||||
|
vmImage: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- task: Bash@3
|
||||||
|
inputs:
|
||||||
|
targetType: "inline"
|
||||||
|
script: |
|
||||||
|
echo "VERSION=$(tag)" >> .env.production
|
||||||
|
echo "ShouldPush=$(shouldPush)"
|
||||||
|
echo "ForcePush=${{ parameters.forcePush }}"
|
||||||
|
echo "isMaster=$(isMaster)"
|
||||||
|
|
||||||
|
- task: AzureCLI@2
|
||||||
|
displayName: Login to ACR
|
||||||
|
inputs:
|
||||||
|
azureSubscription: "mi-devops"
|
||||||
|
scriptType: "bash"
|
||||||
|
scriptLocation: "inlineScript"
|
||||||
|
workingDirectory: "$(build.sourcesDirectory)"
|
||||||
|
inlineScript: az acr login --name acrscandicfrontend
|
||||||
|
|
||||||
|
- task: AzureCLI@2
|
||||||
|
displayName: Build and push to ACR
|
||||||
|
inputs:
|
||||||
|
azureSubscription: "mi-devops"
|
||||||
|
scriptType: "bash"
|
||||||
|
scriptLocation: "inlineScript"
|
||||||
|
workingDirectory: "$(build.sourcesDirectory)"
|
||||||
|
inlineScript: |
|
||||||
|
if [ "$(shouldPush)" != "True" ]; then
|
||||||
|
echo "Not pushing to ACR"
|
||||||
|
noPush="--no-push"
|
||||||
|
else
|
||||||
|
echo "Pushing to ACR"
|
||||||
|
noPush=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "isMaster: $(isMaster)"
|
||||||
|
|
||||||
|
if [ "$(isMaster)" == "True" ]; then
|
||||||
|
echo "Building with latest tag"
|
||||||
|
az acr build . --image $(imageName):latest -r acrscandicfrontend $noPush
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building with $(tag) tag"
|
||||||
|
az acr build . --image $(imageName):$(tag) -r acrscandicfrontend $noPush
|
||||||
44
apps/redis-api/ci/azure-pipelines.deploy.yml
Normal file
44
apps/redis-api/ci/azure-pipelines.deploy.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
trigger: none
|
||||||
|
pr: none
|
||||||
|
|
||||||
|
resources:
|
||||||
|
pipelines:
|
||||||
|
- pipeline: buildPipeline
|
||||||
|
source: "Build App BFF"
|
||||||
|
trigger:
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- main
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: ubuntu-latest
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
- name: containerTag
|
||||||
|
displayName: Select tag to deploy
|
||||||
|
type: string
|
||||||
|
default: "latest"
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: containerTag
|
||||||
|
value: ${{ parameters.containerTag }}
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- stage: Deploy_test
|
||||||
|
variables:
|
||||||
|
- group: "BFF test"
|
||||||
|
jobs:
|
||||||
|
- template: ./azure-pipelines.deploywebapptemplate.yml
|
||||||
|
parameters:
|
||||||
|
environment: test
|
||||||
|
subscriptionId: 1a126a59-4703-4e36-ad7b-2503d36526c0
|
||||||
|
containerTag: $(containerTag)
|
||||||
|
# - stage: Deploy_prod
|
||||||
|
# variables:
|
||||||
|
# - group: 'BFF prod'
|
||||||
|
# jobs:
|
||||||
|
# - template: ./azure-pipelines.deploywebapptemplate.yml
|
||||||
|
# parameters:
|
||||||
|
# environment: prod
|
||||||
|
# subscriptionId: 1e6ef69e-8719-4924-a311-e66fe00399c7
|
||||||
|
# containerTag: $(containerTag)
|
||||||
75
apps/redis-api/ci/bicep/app/containerApp.bicep
Normal file
75
apps/redis-api/ci/bicep/app/containerApp.bicep
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Environment, EnvironmentVar } from '../types.bicep'
|
||||||
|
|
||||||
|
param environment Environment
|
||||||
|
param location string
|
||||||
|
param containerAppName string
|
||||||
|
param containerImage string
|
||||||
|
param containerPort int
|
||||||
|
param minReplicas int = 1
|
||||||
|
param maxReplicas int = 3
|
||||||
|
param envVars EnvironmentVar[] = []
|
||||||
|
param userAssignedIdentityId string
|
||||||
|
|
||||||
|
resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = {
|
||||||
|
name: 'acrscandicfrontend'
|
||||||
|
scope: resourceGroup('1e6ef69e-8719-4924-a311-e66fe00399c7', 'rg-shared')
|
||||||
|
}
|
||||||
|
|
||||||
|
resource containerApp 'Microsoft.App/containerApps@2024-10-02-preview' = {
|
||||||
|
name: containerAppName
|
||||||
|
location: location
|
||||||
|
identity: {
|
||||||
|
type: 'UserAssigned'
|
||||||
|
userAssignedIdentities: {
|
||||||
|
'${userAssignedIdentityId}': {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties: {
|
||||||
|
environmentId: resourceId('Microsoft.App/managedEnvironments', 'cae-redis-api-${environment}')
|
||||||
|
configuration: {
|
||||||
|
activeRevisionsMode: 'Single'
|
||||||
|
registries: [
|
||||||
|
{
|
||||||
|
identity: userAssignedIdentityId
|
||||||
|
server: acr.properties.loginServer
|
||||||
|
}
|
||||||
|
]
|
||||||
|
ingress: {
|
||||||
|
external: true
|
||||||
|
targetPort: containerPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template: {
|
||||||
|
containers: [
|
||||||
|
{
|
||||||
|
name: containerAppName
|
||||||
|
image: containerImage
|
||||||
|
imageType: 'ContainerImage'
|
||||||
|
env: [
|
||||||
|
for envVar in envVars: {
|
||||||
|
name: envVar.name
|
||||||
|
value: envVar.value
|
||||||
|
}
|
||||||
|
]
|
||||||
|
probes: [
|
||||||
|
{
|
||||||
|
type: 'Liveness'
|
||||||
|
httpGet: {
|
||||||
|
port: containerPort
|
||||||
|
path: '/health'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
resources: {
|
||||||
|
cpu: json('0.25')
|
||||||
|
memory: '0.5Gi'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
scale: {
|
||||||
|
minReplicas: minReplicas
|
||||||
|
maxReplicas: maxReplicas
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
apps/redis-api/ci/bicep/app/main.bicep
Normal file
42
apps/redis-api/ci/bicep/app/main.bicep
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Environment, EnvironmentVar } from '../types.bicep'
|
||||||
|
|
||||||
|
targetScope = 'subscription'
|
||||||
|
|
||||||
|
param environment Environment
|
||||||
|
param containerImageTag string
|
||||||
|
param redisConnection string
|
||||||
|
param primaryApiKey string
|
||||||
|
param secondaryApiKey string
|
||||||
|
|
||||||
|
param timestamp string = utcNow()
|
||||||
|
|
||||||
|
@description('The location for the resource group')
|
||||||
|
param location string = 'westeurope'
|
||||||
|
|
||||||
|
resource rgRedisApi 'Microsoft.Resources/resourceGroups@2021-04-01' existing = {
|
||||||
|
name: 'rg-redis-api-${environment}'
|
||||||
|
}
|
||||||
|
|
||||||
|
resource mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
|
||||||
|
name: 'mi-redis-api-${environment}'
|
||||||
|
scope: rgRedisApi
|
||||||
|
}
|
||||||
|
|
||||||
|
module containerApp 'containerApp.bicep' = {
|
||||||
|
name: 'containerApp'
|
||||||
|
scope: rgRedisApi
|
||||||
|
params: {
|
||||||
|
location: location
|
||||||
|
environment: environment
|
||||||
|
userAssignedIdentityId: mi.id
|
||||||
|
containerAppName: 'ca-redis-api-${environment}'
|
||||||
|
containerImage: 'acrscandicfrontend.azurecr.io/redis-api:${containerImageTag}'
|
||||||
|
containerPort: 3001
|
||||||
|
envVars: [
|
||||||
|
{ name: 'REDIS_CONNECTION', value: redisConnection }
|
||||||
|
{ name: 'PRIMARY_API_KEY', value: primaryApiKey }
|
||||||
|
{ name: 'SECONDARY_API_KEY', value: secondaryApiKey }
|
||||||
|
{ name: 'timestamp', value: timestamp }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
49
apps/redis-api/ci/bicep/cache/redis.bicep
vendored
Normal file
49
apps/redis-api/ci/bicep/cache/redis.bicep
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Environment } from '../types.bicep'
|
||||||
|
|
||||||
|
param environment Environment
|
||||||
|
|
||||||
|
@description('The location for the resource group')
|
||||||
|
param location string = 'westeurope'
|
||||||
|
|
||||||
|
var testSKU = {
|
||||||
|
name: 'Basic'
|
||||||
|
family: 'C'
|
||||||
|
capacity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var prodSKU = {
|
||||||
|
name: 'Standard'
|
||||||
|
family: 'C'
|
||||||
|
capacity: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var sku = environment == 'prod' ? prodSKU : testSKU
|
||||||
|
|
||||||
|
resource redisResource 'Microsoft.Cache/Redis@2024-11-01' = {
|
||||||
|
name: 'redis-scandic-frontend-${environment}'
|
||||||
|
location: location
|
||||||
|
properties: {
|
||||||
|
redisVersion: '6.0'
|
||||||
|
sku: {
|
||||||
|
name: sku.name
|
||||||
|
family: sku.family
|
||||||
|
capacity: sku.capacity
|
||||||
|
}
|
||||||
|
enableNonSslPort: false
|
||||||
|
minimumTlsVersion: '1.2'
|
||||||
|
publicNetworkAccess: 'Enabled'
|
||||||
|
redisConfiguration: {
|
||||||
|
'aad-enabled': 'false'
|
||||||
|
'maxmemory-reserved': '30'
|
||||||
|
'maxfragmentationmemory-reserved': '30'
|
||||||
|
'maxmemory-delta': '30'
|
||||||
|
}
|
||||||
|
updateChannel: 'Stable'
|
||||||
|
disableAccessKeyAuthentication: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output hostname string = redisResource.properties.hostName
|
||||||
|
output connectionString string = '${redisResource.properties.hostName}:6380,password=${redisResource.properties.accessKeys.primaryKey},ssl=True,abortConnect=False'
|
||||||
|
|
||||||
|
output primaryAccessKey string = redisResource.properties.accessKeys.primaryKey
|
||||||
19
apps/redis-api/ci/bicep/infra/allow-acr-pull.bicep
Normal file
19
apps/redis-api/ci/bicep/infra/allow-acr-pull.bicep
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
param principalId string
|
||||||
|
|
||||||
|
module acrPull '../roles/acr-pull.bicep' = {
|
||||||
|
name: 'acrPull'
|
||||||
|
}
|
||||||
|
|
||||||
|
resource registry 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = {
|
||||||
|
name: 'acrscandicfrontend'
|
||||||
|
}
|
||||||
|
|
||||||
|
resource rbac 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
||||||
|
name: guid(registry.name, 'ServicePrincipal', principalId, acrPull.name)
|
||||||
|
scope: registry
|
||||||
|
properties: {
|
||||||
|
principalType: 'ServicePrincipal'
|
||||||
|
principalId: principalId
|
||||||
|
roleDefinitionId: acrPull.outputs.id
|
||||||
|
}
|
||||||
|
}
|
||||||
26
apps/redis-api/ci/bicep/infra/containerEnvironment.bicep
Normal file
26
apps/redis-api/ci/bicep/infra/containerEnvironment.bicep
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Environment } from '../types.bicep'
|
||||||
|
|
||||||
|
param location string = 'westeurope'
|
||||||
|
param environment Environment
|
||||||
|
param userAssignedIdentityId string
|
||||||
|
|
||||||
|
resource containerEnv 'Microsoft.App/managedEnvironments@2024-02-02-preview' = {
|
||||||
|
name: 'cae-redis-api-${environment}'
|
||||||
|
location: location
|
||||||
|
identity: {
|
||||||
|
type: 'UserAssigned'
|
||||||
|
userAssignedIdentities: {
|
||||||
|
'${userAssignedIdentityId}': {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties: {
|
||||||
|
publicNetworkAccess: 'Enabled'
|
||||||
|
workloadProfiles: [
|
||||||
|
{
|
||||||
|
name: 'Consumption'
|
||||||
|
workloadProfileType: 'Consumption'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
zoneRedundant: false
|
||||||
|
}
|
||||||
|
}
|
||||||
40
apps/redis-api/ci/bicep/infra/main.bicep
Normal file
40
apps/redis-api/ci/bicep/infra/main.bicep
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Environment } from '../types.bicep'
|
||||||
|
|
||||||
|
targetScope = 'subscription'
|
||||||
|
|
||||||
|
param environment Environment
|
||||||
|
|
||||||
|
var location = deployment().location
|
||||||
|
var productionSubscriptionId = '799cbffe-5209-41fd-adf9-4ffa3d1feead'
|
||||||
|
|
||||||
|
resource rgBff 'Microsoft.Resources/resourceGroups@2021-04-01' = {
|
||||||
|
name: 'rg-redis-api-${environment}'
|
||||||
|
location: location
|
||||||
|
}
|
||||||
|
|
||||||
|
module mi '../managedIdentity.bicep' = {
|
||||||
|
name: 'mi-redis-api-${environment}'
|
||||||
|
scope: rgBff
|
||||||
|
params: {
|
||||||
|
principalName: 'mi-redis-api-${environment}'
|
||||||
|
location: location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module allowAcrPull 'allow-acr-pull.bicep' = {
|
||||||
|
name: 'allowAcrPull'
|
||||||
|
scope: resourceGroup('1e6ef69e-8719-4924-a311-e66fe00399c7', 'rg-shared')
|
||||||
|
params: {
|
||||||
|
principalId: mi.outputs.principalId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module containerEnv 'containerEnvironment.bicep' = {
|
||||||
|
name: 'containerEnv'
|
||||||
|
scope: rgBff
|
||||||
|
params: {
|
||||||
|
location: location
|
||||||
|
environment: environment
|
||||||
|
userAssignedIdentityId: mi.outputs.id
|
||||||
|
}
|
||||||
|
}
|
||||||
47
apps/redis-api/ci/bicep/main.bicep
Normal file
47
apps/redis-api/ci/bicep/main.bicep
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Environment, EnvironmentVar } from 'types.bicep'
|
||||||
|
|
||||||
|
targetScope = 'subscription'
|
||||||
|
|
||||||
|
param environment Environment
|
||||||
|
param containerImageTag string = 'latest'
|
||||||
|
|
||||||
|
param primaryApiKey string
|
||||||
|
param secondaryApiKey string
|
||||||
|
|
||||||
|
@description('The location for the resource group')
|
||||||
|
param location string = 'westeurope'
|
||||||
|
|
||||||
|
resource rgRedisApi 'Microsoft.Resources/resourceGroups@2021-04-01' = {
|
||||||
|
name: 'rg-redis-api-${environment}'
|
||||||
|
location: location
|
||||||
|
}
|
||||||
|
|
||||||
|
module mi 'managedIdentity.bicep' = {
|
||||||
|
name: 'mi-redis-api-${environment}'
|
||||||
|
scope: rgRedisApi
|
||||||
|
params: {
|
||||||
|
principalName: 'mi-redis-api-${environment}'
|
||||||
|
location: location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module redis 'cache/redis.bicep' = {
|
||||||
|
name: 'redisCache'
|
||||||
|
scope: rgRedisApi
|
||||||
|
params: {
|
||||||
|
location: location
|
||||||
|
environment: environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module containerApp 'app/main.bicep' = {
|
||||||
|
name: 'containerApp'
|
||||||
|
params: {
|
||||||
|
location: location
|
||||||
|
environment: environment
|
||||||
|
containerImageTag: containerImageTag
|
||||||
|
redisConnection: 'default:${redis.outputs.primaryAccessKey}@${redis.outputs.hostname}:6380'
|
||||||
|
primaryApiKey: primaryApiKey
|
||||||
|
secondaryApiKey: secondaryApiKey
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/redis-api/ci/bicep/managedIdentity.bicep
Normal file
10
apps/redis-api/ci/bicep/managedIdentity.bicep
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
param location string = 'westeurope'
|
||||||
|
param principalName string
|
||||||
|
|
||||||
|
resource mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
|
||||||
|
name: principalName
|
||||||
|
location: location
|
||||||
|
}
|
||||||
|
|
||||||
|
output principalId string = mi.properties.principalId
|
||||||
|
output id string = mi.id
|
||||||
5
apps/redis-api/ci/bicep/roles/acr-pull.bicep
Normal file
5
apps/redis-api/ci/bicep/roles/acr-pull.bicep
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@description('Pull artifacts from a container registry. Ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/containers#acrpull')
|
||||||
|
resource rd 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview' existing = {
|
||||||
|
name: '7f951dda-4ed3-4680-a7ca-43fe172d538d'
|
||||||
|
}
|
||||||
|
output id string = rd.id
|
||||||
9
apps/redis-api/ci/bicep/types.bicep
Normal file
9
apps/redis-api/ci/bicep/types.bicep
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@export()
|
||||||
|
@description('Type with allowed environments.')
|
||||||
|
type Environment = 'test' | 'prod'
|
||||||
|
|
||||||
|
@export()
|
||||||
|
type EnvironmentVar = {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
23
apps/redis-api/package.json
Normal file
23
apps/redis-api/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "redis-api",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun --watch src/index.ts | pino-pretty -o '{if module}[{module}] {end}{msg}' -i pid,hostname"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@elysiajs/server-timing": "1.2.1",
|
||||||
|
"@elysiajs/swagger": "1.2.2",
|
||||||
|
"@t3-oss/env-core": "0.12.0",
|
||||||
|
"elysia": "1.2.25",
|
||||||
|
"ioredis": "5.6.0",
|
||||||
|
"pino": "9.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.4",
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"pino-pretty": "^13.0.0",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/redis-api/scripts/generateApiKeys.ts
Normal file
13
apps/redis-api/scripts/generateApiKeys.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
function generateApiKey(length = 32): string {
|
||||||
|
return crypto.randomBytes(length).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this file is run directly, generate and log an API key.
|
||||||
|
if (require.main === module) {
|
||||||
|
console.log("Primary API Key:", generateApiKey());
|
||||||
|
console.log("Secondary API Key:", generateApiKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
export { generateApiKey };
|
||||||
56
apps/redis-api/src/env.ts
Normal file
56
apps/redis-api/src/env.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { createEnv } from "@t3-oss/env-core";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const redisConnectionRegex =
|
||||||
|
/^((?<username>.*?):(?<password>.*?)@)?(?<host>.*?):(?<port>\d+)$/;
|
||||||
|
export const env = createEnv({
|
||||||
|
server: {
|
||||||
|
IS_PROD: z
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.transform(
|
||||||
|
() =>
|
||||||
|
process.env.BUN_ENV === "production" ||
|
||||||
|
process.env.NODE_ENV === "production"
|
||||||
|
),
|
||||||
|
IS_DEV: z
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.transform(
|
||||||
|
() =>
|
||||||
|
process.env.BUN_ENV === "development" ||
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
),
|
||||||
|
VERSION: z.string().min(1).default("development"),
|
||||||
|
PORT: z.coerce.number().default(3001),
|
||||||
|
REDIS_CONNECTION: z.string().regex(redisConnectionRegex),
|
||||||
|
|
||||||
|
PRIMARY_API_KEY:
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? z.string().optional()
|
||||||
|
: z.string().min(10),
|
||||||
|
|
||||||
|
SECONDARY_API_KEY:
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? z.string().optional()
|
||||||
|
: z.string().min(10),
|
||||||
|
},
|
||||||
|
runtimeEnv: {
|
||||||
|
...process.env,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const redisMatch = env.REDIS_CONNECTION.match(redisConnectionRegex);
|
||||||
|
if (!redisMatch?.groups) {
|
||||||
|
throw new Error("Invalid REDIS_CONNECTION format");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const redisConfig = {
|
||||||
|
host: redisMatch.groups.host,
|
||||||
|
port: Number(redisMatch.groups.port),
|
||||||
|
username: redisMatch.groups.username,
|
||||||
|
password: redisMatch.groups.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("env", env);
|
||||||
|
console.log("redisConfig", redisConfig);
|
||||||
6
apps/redis-api/src/errors/AuthenticationError.ts
Normal file
6
apps/redis-api/src/errors/AuthenticationError.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export class AuthenticationError extends Error {
|
||||||
|
constructor(public message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "AuthenticationError";
|
||||||
|
}
|
||||||
|
}
|
||||||
6
apps/redis-api/src/errors/ModelValidationError.ts
Normal file
6
apps/redis-api/src/errors/ModelValidationError.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export class ModelValidationError extends Error {
|
||||||
|
constructor(public message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ModelValidationError";
|
||||||
|
}
|
||||||
|
}
|
||||||
59
apps/redis-api/src/index.ts
Normal file
59
apps/redis-api/src/index.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Elysia } from "elysia";
|
||||||
|
|
||||||
|
import { swagger } from "@elysiajs/swagger";
|
||||||
|
import { apiRoutes } from "@/routes/api";
|
||||||
|
import { healthRoutes } from "@/routes/health";
|
||||||
|
import { baseLogger } from "@/utils/logger";
|
||||||
|
import { env } from "@/env";
|
||||||
|
import serverTiming from "@elysiajs/server-timing";
|
||||||
|
import { AuthenticationError } from "@/errors/AuthenticationError";
|
||||||
|
import { ModelValidationError } from "@/errors/ModelValidationError";
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(serverTiming())
|
||||||
|
.error("AUTHENTICATION_ERROR", AuthenticationError)
|
||||||
|
.error("MODEL_VALIDATION_ERROR", ModelValidationError)
|
||||||
|
|
||||||
|
.onError(({ code, error, set }) => {
|
||||||
|
switch (code) {
|
||||||
|
case "MODEL_VALIDATION_ERROR":
|
||||||
|
set.status = 400;
|
||||||
|
return getErrorReturn(error);
|
||||||
|
case "AUTHENTICATION_ERROR":
|
||||||
|
set.status = 401;
|
||||||
|
return getErrorReturn(error);
|
||||||
|
case "NOT_FOUND":
|
||||||
|
set.status = 404;
|
||||||
|
return getErrorReturn(error);
|
||||||
|
case "INTERNAL_SERVER_ERROR":
|
||||||
|
set.status = 500;
|
||||||
|
return getErrorReturn(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (env.IS_DEV) {
|
||||||
|
app.use(
|
||||||
|
swagger({
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: "Redis API",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(apiRoutes);
|
||||||
|
app.use(healthRoutes);
|
||||||
|
|
||||||
|
app.listen(env.PORT, (server) => {
|
||||||
|
baseLogger.info(`🦊 REDISAPI@${env.VERSION} running on ${server.url}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getErrorReturn(error: Error) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: error.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
28
apps/redis-api/src/middleware/apiKeyMiddleware.ts
Normal file
28
apps/redis-api/src/middleware/apiKeyMiddleware.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { AuthenticationError } from "@/errors/AuthenticationError";
|
||||||
|
import type { Context } from "elysia";
|
||||||
|
import { env } from "@/env";
|
||||||
|
|
||||||
|
const API_KEY_HEADER = "x-api-key";
|
||||||
|
|
||||||
|
export const apiKeyMiddleware = ({ headers }: Context) => {
|
||||||
|
if (!isApiKeyRequired()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = headers[API_KEY_HEADER];
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new AuthenticationError("No API KEY provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateApiKey(apiKey)) {
|
||||||
|
throw new AuthenticationError("Invalid API key");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function isApiKeyRequired(): boolean {
|
||||||
|
return Boolean(env.PRIMARY_API_KEY) || Boolean(env.SECONDARY_API_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateApiKey(apiKey: string): boolean {
|
||||||
|
return apiKey === env.PRIMARY_API_KEY || apiKey === env.SECONDARY_API_KEY;
|
||||||
|
}
|
||||||
93
apps/redis-api/src/routes/api/cache.ts
Normal file
93
apps/redis-api/src/routes/api/cache.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Elysia, t, ValidationError } from "elysia";
|
||||||
|
import { redis } from "@/services/redis";
|
||||||
|
import { ModelValidationError } from "@/errors/ModelValidationError";
|
||||||
|
|
||||||
|
const MIN_LENGTH = 1;
|
||||||
|
|
||||||
|
const QUERY_TYPE = t.Object({ key: t.String({ minLength: MIN_LENGTH }) });
|
||||||
|
|
||||||
|
export const cacheRoutes = new Elysia({ prefix: "/cache" })
|
||||||
|
.get(
|
||||||
|
"/",
|
||||||
|
async ({ query: { key }, error }) => {
|
||||||
|
key = validateKey(key);
|
||||||
|
console.log("GET /cache", key);
|
||||||
|
|
||||||
|
const value = await redis.get(key);
|
||||||
|
if (!value) {
|
||||||
|
return error("Not Found", "Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const output = JSON.parse(value);
|
||||||
|
return { data: output };
|
||||||
|
} catch (e) {
|
||||||
|
redis.del(key);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: QUERY_TYPE,
|
||||||
|
response: { 200: t.Object({ data: t.Any() }), 404: t.String() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"/",
|
||||||
|
async ({ query: { key }, body, error, set }) => {
|
||||||
|
key = validateKey(key);
|
||||||
|
console.log("PUT /cache", key);
|
||||||
|
|
||||||
|
if (!body.ttl || body.ttl < 0) {
|
||||||
|
return error("Bad Request", "ttl is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.set(key, JSON.stringify(body.data), "EX", body.ttl);
|
||||||
|
|
||||||
|
set.status = 204;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({ data: t.Any(), ttl: t.Number() }),
|
||||||
|
query: QUERY_TYPE,
|
||||||
|
response: { 204: t.Void(), 400: t.String() },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.delete(
|
||||||
|
"/",
|
||||||
|
async ({ query: { key, fuzzy }, set }) => {
|
||||||
|
key = validateKey(key);
|
||||||
|
console.log("DELETE /cache", key);
|
||||||
|
|
||||||
|
if (fuzzy) {
|
||||||
|
key = `*${key}*`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.del(key);
|
||||||
|
|
||||||
|
set.status = 204;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: t.Object({
|
||||||
|
...QUERY_TYPE.properties,
|
||||||
|
...t.Object({ fuzzy: t.Optional(t.Boolean()) }).properties,
|
||||||
|
}),
|
||||||
|
response: { 204: t.Void(), 400: t.String() },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function validateKey(key: string) {
|
||||||
|
const parsedKey = decodeURIComponent(key);
|
||||||
|
|
||||||
|
if (parsedKey.length < MIN_LENGTH) {
|
||||||
|
throw new ModelValidationError(
|
||||||
|
"Key has to be atleast 1 character long"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedKey.includes("*")) {
|
||||||
|
throw new ModelValidationError("Key cannot contain wildcards");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedKey;
|
||||||
|
}
|
||||||
7
apps/redis-api/src/routes/api/index.ts
Normal file
7
apps/redis-api/src/routes/api/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Elysia } from "elysia";
|
||||||
|
import { cacheRoutes } from "./cache";
|
||||||
|
import { apiKeyMiddleware } from "@/middleware/apiKeyMiddleware";
|
||||||
|
|
||||||
|
export const apiRoutes = new Elysia({ prefix: "/api" })
|
||||||
|
.guard({ beforeHandle: apiKeyMiddleware })
|
||||||
|
.use(cacheRoutes);
|
||||||
34
apps/redis-api/src/routes/health.ts
Normal file
34
apps/redis-api/src/routes/health.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
|
||||||
|
import { redis } from "@/services/redis";
|
||||||
|
import { baseLogger } from "@/utils/logger";
|
||||||
|
|
||||||
|
export const healthRoutes = new Elysia().get(
|
||||||
|
"/health",
|
||||||
|
async ({ set, error }) => {
|
||||||
|
const perf = performance.now();
|
||||||
|
try {
|
||||||
|
await redis.ping();
|
||||||
|
} catch (e) {
|
||||||
|
baseLogger.error("Redis connection error:", e);
|
||||||
|
console.log("Redis connection error:", e);
|
||||||
|
|
||||||
|
return error(503, { healthy: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = performance.now() - perf;
|
||||||
|
baseLogger.info(`Service healthy: ${duration.toFixed(2)} ms`);
|
||||||
|
|
||||||
|
return { healthy: true };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
response: {
|
||||||
|
200: t.Object({
|
||||||
|
healthy: t.Boolean(),
|
||||||
|
}),
|
||||||
|
503: t.Object({
|
||||||
|
healthy: t.Boolean(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
19
apps/redis-api/src/services/redis.ts
Normal file
19
apps/redis-api/src/services/redis.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { redisConfig, env } from "@/env";
|
||||||
|
import ioredis from "ioredis";
|
||||||
|
|
||||||
|
const redis = new ioredis({
|
||||||
|
host: redisConfig.host,
|
||||||
|
port: redisConfig.port,
|
||||||
|
username: redisConfig.username,
|
||||||
|
password: redisConfig.password,
|
||||||
|
maxRetriesPerRequest: 1, // Avoid excessive retries,
|
||||||
|
tls: !env.IS_DEV
|
||||||
|
? {
|
||||||
|
rejectUnauthorized: true,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
lazyConnect: true,
|
||||||
|
connectTimeout: 10_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { redis };
|
||||||
34
apps/redis-api/src/utils/logger.ts
Normal file
34
apps/redis-api/src/utils/logger.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import pino from "pino";
|
||||||
|
import { mask } from "./mask";
|
||||||
|
import { env } from "@/env";
|
||||||
|
|
||||||
|
const serializers: { [key: string]: pino.SerializerFn } = {
|
||||||
|
password: (payload) => {
|
||||||
|
if (payload) {
|
||||||
|
return env.IS_DEV
|
||||||
|
? mask(payload)
|
||||||
|
: mask(payload, {
|
||||||
|
visibleStart: 0,
|
||||||
|
visibleEnd: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
email: (payload) => {
|
||||||
|
if (payload) {
|
||||||
|
return env.IS_DEV ? payload : mask(payload);
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const baseLogger = pino({
|
||||||
|
level: process.env.LOG_LEVEL || "info",
|
||||||
|
timestamp: pino.stdTimeFunctions.isoTime,
|
||||||
|
serializers,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loggerModule = (loggerName: string) => {
|
||||||
|
return baseLogger.child({ module: loggerName });
|
||||||
|
};
|
||||||
42
apps/redis-api/src/utils/mask.test.ts
Normal file
42
apps/redis-api/src/utils/mask.test.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import { mask } from "./mask";
|
||||||
|
|
||||||
|
describe("mask", () => {
|
||||||
|
it("should return empty string for empty input", () => {
|
||||||
|
expect(mask("")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should mask string with default parameters", () => {
|
||||||
|
expect(mask("1234567890")).toBe("12******90");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show custom number of characters at start", () => {
|
||||||
|
expect(mask("1234567890", { visibleStart: 3 })).toBe("123*****90");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show custom number of characters at end", () => {
|
||||||
|
expect(mask("1234567890", { visibleStart: 2, visibleEnd: 3 })).toBe(
|
||||||
|
"12*****890",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should mask entire string when visible parts exceed length", () => {
|
||||||
|
expect(mask("123", { visibleStart: 2, visibleEnd: 2 })).toBe("***");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle undefined end part", () => {
|
||||||
|
expect(mask("1234567890", { visibleStart: 2, visibleEnd: 0 })).toBe(
|
||||||
|
"12********",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle long strings", () => {
|
||||||
|
expect(mask("12345678901234567890")).toBe("12**********90");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle emails", () => {
|
||||||
|
expect(mask("test.testsson@scandichotels.com")).toBe(
|
||||||
|
"te*********on@sc*********ls.com",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
42
apps/redis-api/src/utils/mask.ts
Normal file
42
apps/redis-api/src/utils/mask.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Masks a string by replacing characters with a mask character
|
||||||
|
* @param value - The string to mask
|
||||||
|
* @param visibleStart - Number of characters to show at start (default: 0)
|
||||||
|
* @param visibleEnd - Number of characters to show at end (default: 4)
|
||||||
|
* @param maskChar - Character to use for masking (default: '*')
|
||||||
|
* @returns The masked string
|
||||||
|
*/
|
||||||
|
const maskChar = "*";
|
||||||
|
export function mask(
|
||||||
|
value: string,
|
||||||
|
options?: { visibleStart?: number; visibleEnd?: number; maxLength?: number },
|
||||||
|
): string {
|
||||||
|
if (!value) return "";
|
||||||
|
|
||||||
|
const { visibleStart = 2, visibleEnd = 2, maxLength = 10 } = options ?? {};
|
||||||
|
|
||||||
|
if (isEmail(value)) {
|
||||||
|
return maskEmail(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalVisible = visibleStart + visibleEnd;
|
||||||
|
if (value.length <= totalVisible) {
|
||||||
|
return maskChar.repeat(value.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = value.slice(0, visibleStart);
|
||||||
|
const middle = value.slice(visibleStart, -visibleEnd || undefined);
|
||||||
|
const end = visibleEnd ? value.slice(-visibleEnd) : "";
|
||||||
|
|
||||||
|
const maskedLength = Math.min(middle.length, maxLength);
|
||||||
|
return start + maskChar.repeat(maskedLength) + end;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskEmail(email: string): string {
|
||||||
|
const [local, domain] = email.split("@");
|
||||||
|
if (!domain || !local) return mask(email);
|
||||||
|
const [subDomain, tld] = domain.split(/\.(?=[^.]+$)/);
|
||||||
|
return `${mask(local)}@${mask(subDomain ?? "")}.${tld}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||||||
30
apps/redis-api/tsconfig.json
Normal file
30
apps/redis-api/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable latest features
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,3 +12,5 @@ netlify.toml
|
|||||||
package.json
|
package.json
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.gitignore
|
.gitignore
|
||||||
|
*.bicep
|
||||||
|
*.ico
|
||||||
@@ -3,9 +3,11 @@
|
|||||||
The web is using OAuth 2.0 to handle auth. We host our own instance of [Curity](https://curity.io), which is our identity and access management solution.
|
The web is using OAuth 2.0 to handle auth. We host our own instance of [Curity](https://curity.io), which is our identity and access management solution.
|
||||||
|
|
||||||
## Session management in Next
|
## Session management in Next
|
||||||
|
|
||||||
We use [Auth.js](https://authjs.dev) to handle everything regarding auth in the web. We use the JWT session strategy, which means that everything regarding the session is stored in a JWT, which is stored in the browser in an encrypted cookie.
|
We use [Auth.js](https://authjs.dev) to handle everything regarding auth in the web. We use the JWT session strategy, which means that everything regarding the session is stored in a JWT, which is stored in the browser in an encrypted cookie.
|
||||||
|
|
||||||
## Keeping the access token alive
|
## Keeping the access token alive
|
||||||
|
|
||||||
When the user performs a navigation the web app often does multiple requests to Next. If the access token has expired Next will do a request to Curity to renew the tokens. Since we only allow a single refresh token to be used only once only the first request will succeed and the following requests will fail.
|
When the user performs a navigation the web app often does multiple requests to Next. If the access token has expired Next will do a request to Curity to renew the tokens. Since we only allow a single refresh token to be used only once only the first request will succeed and the following requests will fail.
|
||||||
|
|
||||||
To avoid that we have a component whose only purpose is to keep the access token alive. As long as no other request is happening at the same time this will work fine.
|
To avoid that we have a component whose only purpose is to keep the access token alive. As long as no other request is happening at the same time this will work fine.
|
||||||
|
|||||||
@@ -18,6 +18,19 @@ yarn dev
|
|||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
You have the choice to either use redis (via redis-api; a tiny http proxy) or in-memory/unstable_cache (depending on edge or node).
|
||||||
|
Setting `REDIS_API_HOST` will configure it to use the distributed cache, not providing it will fall back to in-memory/unstable_cache
|
||||||
|
When pointing to the azure hosted variant you also need to provide `REDIS_API_KEY`
|
||||||
|
|
||||||
|
Locally it's easiest is to spin everything up using docker/podman - `podman compose up` or `docker-compose up`
|
||||||
|
This will also spin up [Redis Insight ](https://redis.io/insight/) so that you can debug the cache.
|
||||||
|
|
||||||
|
- Navigate to `http://localhost:5540`
|
||||||
|
- Click **'Add Redis database'**
|
||||||
|
- Provide Connection URL `redis://redis:6379`
|
||||||
|
|
||||||
## Learn More
|
## Learn More
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { startTransition, useEffect, useRef } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { login } from "@/constants/routes/handleAuth"
|
import { login } from "@/constants/routes/handleAuth"
|
||||||
|
import { env } from "@/env/client"
|
||||||
import { SESSION_EXPIRED } from "@/server/errors/trpc"
|
import { SESSION_EXPIRED } from "@/server/errors/trpc"
|
||||||
|
|
||||||
import styles from "./error.module.css"
|
import styles from "./error.module.css"
|
||||||
@@ -61,6 +62,9 @@ export default function Error({
|
|||||||
<section className={styles.layout}>
|
<section className={styles.layout}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{intl.formatMessage({ id: "Something went wrong!" })}
|
{intl.formatMessage({ id: "Something went wrong!" })}
|
||||||
|
{env.NEXT_PUBLIC_NODE_ENV === "development" && (
|
||||||
|
<pre>{error.stack || error.message}</pre>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ export default async function CurrentContentPage({
|
|||||||
{
|
{
|
||||||
locale: params.lang,
|
locale: params.lang,
|
||||||
url: searchParams.uri,
|
url: searchParams.uri,
|
||||||
},
|
}
|
||||||
{ cache: "no-store" }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!response.data?.all_current_blocks_page?.total) {
|
if (!response.data?.all_current_blocks_page?.total) {
|
||||||
@@ -39,8 +38,7 @@ export default async function CurrentContentPage({
|
|||||||
// This is currently to be considered a temporary solution to provide the tracking with a few values in english to align with existing reports
|
// This is currently to be considered a temporary solution to provide the tracking with a few values in english to align with existing reports
|
||||||
const pageDataForTracking = await request<TrackingData>(
|
const pageDataForTracking = await request<TrackingData>(
|
||||||
GetCurrentBlockPageTrackingData,
|
GetCurrentBlockPageTrackingData,
|
||||||
{ uid: response.data.all_current_blocks_page.items[0].system.uid },
|
{ uid: response.data.all_current_blocks_page.items[0].system.uid }
|
||||||
{ cache: "no-store" }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const pageData = response.data.all_current_blocks_page.items[0]
|
const pageData = response.data.all_current_blocks_page.items[0]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Lang } from "@/constants/languages"
|
|||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { badRequest, internalServerError, notFound } from "@/server/errors/next"
|
import { badRequest, internalServerError, notFound } from "@/server/errors/next"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
import { generateHotelUrlTag } from "@/utils/generateTag"
|
import { generateHotelUrlTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
import type { NextRequest } from "next/server"
|
||||||
@@ -63,6 +64,8 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
console.info(`Revalidating hotel url tag: ${tag}`)
|
console.info(`Revalidating hotel url tag: ${tag}`)
|
||||||
revalidateTag(tag)
|
revalidateTag(tag)
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
await cacheClient.deleteKey(tag, { fuzzy: true })
|
||||||
|
|
||||||
return Response.json({ revalidated: true, now: Date.now() })
|
return Response.json({ revalidated: true, now: Date.now() })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Lang } from "@/constants/languages"
|
|||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { badRequest, internalServerError, notFound } from "@/server/errors/next"
|
import { badRequest, internalServerError, notFound } from "@/server/errors/next"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
|
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
import type { NextRequest } from "next/server"
|
||||||
@@ -82,6 +83,9 @@ export async function POST(request: NextRequest) {
|
|||||||
console.info(`Revalidating loyalty config tag: ${tag}`)
|
console.info(`Revalidating loyalty config tag: ${tag}`)
|
||||||
revalidateTag(tag)
|
revalidateTag(tag)
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
await cacheClient.deleteKey(tag, { fuzzy: true })
|
||||||
|
|
||||||
return Response.json({ revalidated: true, now: Date.now() })
|
return Response.json({ revalidated: true, now: Date.now() })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to revalidate tag(s) for loyalty config")
|
console.error("Failed to revalidate tag(s) for loyalty config")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { headers } from "next/headers"
|
|||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { badRequest, internalServerError } from "@/server/errors/next"
|
import { badRequest, internalServerError } from "@/server/errors/next"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
import { generateTag } from "@/utils/generateTag"
|
import { generateTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
@@ -27,23 +28,8 @@ export async function POST() {
|
|||||||
const affix = headersList.get("x-affix")
|
const affix = headersList.get("x-affix")
|
||||||
const identifier = headersList.get("x-identifier")
|
const identifier = headersList.get("x-identifier")
|
||||||
const lang = headersList.get("x-lang")
|
const lang = headersList.get("x-lang")
|
||||||
if (lang && identifier) {
|
|
||||||
if (affix) {
|
if (!lang || !identifier) {
|
||||||
const tag = generateTag(lang as Lang, identifier, affix)
|
|
||||||
console.info(
|
|
||||||
`Revalidated tag for [lang: ${lang}, identifier: ${identifier}, affix: ${affix}]`
|
|
||||||
)
|
|
||||||
console.info(`Tag: ${tag}`)
|
|
||||||
revalidateTag(tag)
|
|
||||||
} else {
|
|
||||||
const tag = generateTag(lang as Lang, identifier)
|
|
||||||
console.info(
|
|
||||||
`Revalidated tag for [lang: ${lang}, identifier: ${identifier}]`
|
|
||||||
)
|
|
||||||
console.info(`Tag: ${tag}`)
|
|
||||||
revalidateTag(tag)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.info(`Missing lang and/or identifier`)
|
console.info(`Missing lang and/or identifier`)
|
||||||
console.info(`lang: ${lang}, identifier: ${identifier}`)
|
console.info(`lang: ${lang}, identifier: ${identifier}`)
|
||||||
return badRequest({
|
return badRequest({
|
||||||
@@ -52,6 +38,18 @@ export async function POST() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
const tag = generateTag(lang as Lang, identifier, affix)
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
`Revalidated tag for [lang: ${lang}, identifier: ${identifier}${affix ? `, affix: ${affix}` : ""}]`
|
||||||
|
)
|
||||||
|
console.info(`Tag: ${tag}`)
|
||||||
|
|
||||||
|
revalidateTag(tag)
|
||||||
|
cacheClient.deleteKey(tag, { fuzzy: true })
|
||||||
|
|
||||||
return Response.json({ revalidated: true, now: Date.now() })
|
return Response.json({ revalidated: true, now: Date.now() })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to revalidate tag(s)")
|
console.error("Failed to revalidate tag(s)")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { languageSwitcherAffix } from "@/server/routers/contentstack/languageSwi
|
|||||||
import { affix as metadataAffix } from "@/server/routers/contentstack/metadata/utils"
|
import { affix as metadataAffix } from "@/server/routers/contentstack/metadata/utils"
|
||||||
import { affix as pageSettingsAffix } from "@/server/routers/contentstack/pageSettings/utils"
|
import { affix as pageSettingsAffix } from "@/server/routers/contentstack/pageSettings/utils"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
import {
|
import {
|
||||||
generateRefsResponseTag,
|
generateRefsResponseTag,
|
||||||
generateRefTag,
|
generateRefTag,
|
||||||
@@ -87,23 +88,31 @@ export async function POST(request: NextRequest) {
|
|||||||
)
|
)
|
||||||
const metadataTag = generateTag(entryLocale, entry.uid, metadataAffix)
|
const metadataTag = generateTag(entryLocale, entry.uid, metadataAffix)
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
console.info(`Revalidating refsTag: ${refsTag}`)
|
console.info(`Revalidating refsTag: ${refsTag}`)
|
||||||
revalidateTag(refsTag)
|
revalidateTag(refsTag)
|
||||||
|
await cacheClient.deleteKey(refsTag, { fuzzy: true })
|
||||||
|
|
||||||
console.info(`Revalidating refTag: ${refTag}`)
|
console.info(`Revalidating refTag: ${refTag}`)
|
||||||
revalidateTag(refTag)
|
revalidateTag(refTag)
|
||||||
|
await cacheClient.deleteKey(refTag, { fuzzy: true })
|
||||||
|
|
||||||
console.info(`Revalidating tag: ${tag}`)
|
console.info(`Revalidating tag: ${tag}`)
|
||||||
revalidateTag(tag)
|
revalidateTag(tag)
|
||||||
|
await cacheClient.deleteKey(tag, { fuzzy: true })
|
||||||
|
|
||||||
console.info(`Revalidating language switcher tag: ${languageSwitcherTag}`)
|
console.info(`Revalidating language switcher tag: ${languageSwitcherTag}`)
|
||||||
revalidateTag(languageSwitcherTag)
|
revalidateTag(languageSwitcherTag)
|
||||||
|
await cacheClient.deleteKey(languageSwitcherTag, { fuzzy: true })
|
||||||
|
|
||||||
console.info(`Revalidating metadataTag: ${metadataTag}`)
|
console.info(`Revalidating metadataTag: ${metadataTag}`)
|
||||||
revalidateTag(metadataTag)
|
revalidateTag(metadataTag)
|
||||||
|
await cacheClient.deleteKey(metadataTag, { fuzzy: true })
|
||||||
|
|
||||||
console.info(`Revalidating contentEntryTag: ${contentEntryTag}`)
|
console.info(`Revalidating contentEntryTag: ${contentEntryTag}`)
|
||||||
revalidateTag(contentEntryTag)
|
revalidateTag(contentEntryTag)
|
||||||
|
await cacheClient.deleteKey(contentEntryTag, { fuzzy: true })
|
||||||
|
|
||||||
if (entry.breadcrumbs) {
|
if (entry.breadcrumbs) {
|
||||||
const breadcrumbsRefsTag = generateRefsResponseTag(
|
const breadcrumbsRefsTag = generateRefsResponseTag(
|
||||||
@@ -119,9 +128,11 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
console.info(`Revalidating breadcrumbsRefsTag: ${breadcrumbsRefsTag}`)
|
console.info(`Revalidating breadcrumbsRefsTag: ${breadcrumbsRefsTag}`)
|
||||||
revalidateTag(breadcrumbsRefsTag)
|
revalidateTag(breadcrumbsRefsTag)
|
||||||
|
await cacheClient.deleteKey(breadcrumbsRefsTag, { fuzzy: true })
|
||||||
|
|
||||||
console.info(`Revalidating breadcrumbsTag: ${breadcrumbsTag}`)
|
console.info(`Revalidating breadcrumbsTag: ${breadcrumbsTag}`)
|
||||||
revalidateTag(breadcrumbsTag)
|
revalidateTag(breadcrumbsTag)
|
||||||
|
await cacheClient.deleteKey(breadcrumbsTag, { fuzzy: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.page_settings) {
|
if (entry.page_settings) {
|
||||||
@@ -133,6 +144,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
console.info(`Revalidating pageSettingsTag: ${pageSettingsTag}`)
|
console.info(`Revalidating pageSettingsTag: ${pageSettingsTag}`)
|
||||||
revalidateTag(pageSettingsTag)
|
revalidateTag(pageSettingsTag)
|
||||||
|
await cacheClient.deleteKey(pageSettingsTag, { fuzzy: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.json({ revalidated: true, now: Date.now() })
|
return Response.json({ revalidated: true, now: Date.now() })
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function SessionRefresher() {
|
|||||||
const session = useSession()
|
const session = useSession()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const timeoutId = useRef<NodeJS.Timeout>()
|
const timeoutId = useRef<Timer>()
|
||||||
|
|
||||||
// Simple inactivity control. Reset when the URL changes.
|
// Simple inactivity control. Reset when the URL changes.
|
||||||
const stopPreRefreshAt = useMemo(
|
const stopPreRefreshAt = useMemo(
|
||||||
|
|||||||
@@ -15,7 +15,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.partial {
|
.partial {
|
||||||
grid-template-columns: minmax(auto, 150px) min-content minmax(auto, 150px) auto;
|
grid-template-columns: minmax(auto, 150px) min-content minmax(
|
||||||
|
auto,
|
||||||
|
150px
|
||||||
|
) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Map, type MapProps, useMap } from "@vis.gl/react-google-maps"
|
|||||||
import { type PropsWithChildren, useEffect } from "react"
|
import { type PropsWithChildren, useEffect } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import ErrorBoundary from "@/components/ErrorBoundary/ErrorBoundary"
|
||||||
import { CloseLargeIcon, MinusIcon, PlusIcon } from "@/components/Icons"
|
import { CloseLargeIcon, MinusIcon, PlusIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||||
@@ -84,7 +85,9 @@ export default function DynamicMap({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mapWrapper}>
|
<div className={styles.mapWrapper}>
|
||||||
|
<ErrorBoundary fallback={<h2>Unable to display map</h2>}>
|
||||||
<Map {...mapOptions}>{children}</Map>
|
<Map {...mapOptions}>{children}</Map>
|
||||||
|
</ErrorBoundary>
|
||||||
<div className={styles.ctaButtons}>
|
<div className={styles.ctaButtons}>
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -69,8 +69,8 @@
|
|||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.pageContainer {
|
.pageContainer {
|
||||||
--hotel-page-scroll-margin-top: calc(
|
--hotel-page-scroll-margin-top: calc(
|
||||||
var(--hotel-page-navigation-height) + var(--booking-widget-desktop-height) +
|
var(--hotel-page-navigation-height) +
|
||||||
var(--Spacing-x2)
|
var(--booking-widget-desktop-height) + var(--Spacing-x2)
|
||||||
);
|
);
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"header mapContainer"
|
"header mapContainer"
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ export default function OfflineBanner() {
|
|||||||
return (
|
return (
|
||||||
<div className={`${styles.banner} ${styles.hidden}`}>
|
<div className={`${styles.banner} ${styles.hidden}`}>
|
||||||
You are offline, some content may be out of date.
|
You are offline, some content may be out of date.
|
||||||
<button className={styles.reloadBtn} type="button">Reload</button>
|
<button className={styles.reloadBtn} type="button">
|
||||||
|
Reload
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,17 +12,11 @@ export default function Breadcrumbs({
|
|||||||
<ul className={styles.list}>
|
<ul className={styles.list}>
|
||||||
{parent ? (
|
{parent ? (
|
||||||
<li className={styles.parent}>
|
<li className={styles.parent}>
|
||||||
<a href={parent.href}>
|
<a href={parent.href}>{parent.title}</a>
|
||||||
{parent.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
) : null}
|
) : null}
|
||||||
{breadcrumbs.map((breadcrumb) => (
|
{breadcrumbs.map((breadcrumb) => (
|
||||||
<li
|
<li className={styles.li} itemProp="breadcrumb" key={breadcrumb.href}>
|
||||||
className={styles.li}
|
|
||||||
itemProp="breadcrumb"
|
|
||||||
key={breadcrumb.href}
|
|
||||||
>
|
|
||||||
<a className={styles.link} href={breadcrumb.href}>
|
<a className={styles.link} href={breadcrumb.href}>
|
||||||
{breadcrumb.title}
|
{breadcrumb.title}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ export default async function SubnavMobile({
|
|||||||
<ul className="breadcrumb-list hidden-small hidden-medium hidden-large">
|
<ul className="breadcrumb-list hidden-small hidden-medium hidden-large">
|
||||||
{parent ? (
|
{parent ? (
|
||||||
<li className="breadcrumb-list__parent hidden-medium hidden-large">
|
<li className="breadcrumb-list__parent hidden-medium hidden-large">
|
||||||
<a href={parent.href}>
|
<a href={parent.href}>{parent.title}</a>
|
||||||
{parent.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
) : null}
|
) : null}
|
||||||
{breadcrumbs.map((breadcrumb) => (
|
{breadcrumbs.map((breadcrumb) => (
|
||||||
<li className="breadcrumb-list__body" key={breadcrumb.href}>
|
<li className="breadcrumb-list__body" key={breadcrumb.href}>
|
||||||
<a href={breadcrumb.href}>
|
<a href={breadcrumb.href}>{breadcrumb.title}</a>
|
||||||
{breadcrumb.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
<li className="breadcrumb-list__body">
|
<li className="breadcrumb-list__body">
|
||||||
|
|||||||
40
apps/scandic-web/components/ErrorBoundary/ErrorBoundary.tsx
Normal file
40
apps/scandic-web/components/ErrorBoundary/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
type ErrorBoundaryProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
fallback?: React.ReactNode
|
||||||
|
}
|
||||||
|
type ErrorBoundaryState = { hasError: boolean; error?: Error }
|
||||||
|
|
||||||
|
class ErrorBoundary extends React.Component<
|
||||||
|
ErrorBoundaryProps,
|
||||||
|
ErrorBoundaryState
|
||||||
|
> {
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props)
|
||||||
|
this.state = { hasError: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error) {
|
||||||
|
return { hasError: true, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
console.error("ErrorBoundary caught an error:", error, errorInfo)
|
||||||
|
Sentry.captureException(error, { extra: { errorInfo } })
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) {
|
||||||
|
return this.props.fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return <h2>Something went wrong.</h2>
|
||||||
|
}
|
||||||
|
return this.props.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary
|
||||||
@@ -14,7 +14,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.link:nth-of-type(1) .promo {
|
.link:nth-of-type(1) .promo {
|
||||||
background-image: linear-gradient(
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgba(0, 0, 0, 0) 0%,
|
rgba(0, 0, 0, 0) 0%,
|
||||||
rgba(0, 0, 0, 0.36) 37.88%,
|
rgba(0, 0, 0, 0.36) 37.88%,
|
||||||
@@ -24,7 +25,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.link:nth-of-type(2) .promo {
|
.link:nth-of-type(2) .promo {
|
||||||
background-image: linear-gradient(
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgba(0, 0, 0, 0) 0%,
|
rgba(0, 0, 0, 0) 0%,
|
||||||
rgba(0, 0, 0, 0.36) 37.88%,
|
rgba(0, 0, 0, 0.36) 37.88%,
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
|
background-image:
|
||||||
|
linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||||
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
||||||
linear-gradient(45deg, transparent 75%, #000000 75%),
|
linear-gradient(45deg, transparent 75%, #000000 75%),
|
||||||
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.link .promo {
|
.link .promo {
|
||||||
background-image: linear-gradient(
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgba(0, 0, 0, 0) 0%,
|
rgba(0, 0, 0, 0) 0%,
|
||||||
rgba(0, 0, 0, 0.36) 37.88%,
|
rgba(0, 0, 0, 0.36) 37.88%,
|
||||||
|
|||||||
@@ -107,7 +107,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
|
background-image:
|
||||||
|
linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||||
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
||||||
linear-gradient(45deg, transparent 75%, #000000 75%),
|
linear-gradient(45deg, transparent 75%, #000000 75%),
|
||||||
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
||||||
|
|||||||
@@ -18,13 +18,11 @@ import type { Rate } from "@/types/components/hotelReservation/selectRate/select
|
|||||||
|
|
||||||
export default function SelectedRoomPanel() {
|
export default function SelectedRoomPanel() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { isUserLoggedIn, roomCategories } = useRatesStore(
|
const { isUserLoggedIn, roomCategories } = useRatesStore((state) => ({
|
||||||
(state) => ({
|
|
||||||
isUserLoggedIn: state.isUserLoggedIn,
|
isUserLoggedIn: state.isUserLoggedIn,
|
||||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||||
roomCategories: state.roomCategories,
|
roomCategories: state.roomCategories,
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
const {
|
const {
|
||||||
actions: { modifyRate },
|
actions: { modifyRate },
|
||||||
isMainRoom,
|
isMainRoom,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function useRoomsAvailability(
|
|||||||
toDateString: string,
|
toDateString: string,
|
||||||
lang: Lang,
|
lang: Lang,
|
||||||
childArray?: Child[],
|
childArray?: Child[],
|
||||||
bookingCode?: string,
|
bookingCode?: string
|
||||||
) {
|
) {
|
||||||
const returnValue =
|
const returnValue =
|
||||||
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
|
background-image:
|
||||||
|
linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||||
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
||||||
linear-gradient(45deg, transparent 75%, #000000 75%),
|
linear-gradient(45deg, transparent 75%, #000000 75%),
|
||||||
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
||||||
|
|||||||
25
apps/scandic-web/docker-compose.yaml
Normal file
25
apps/scandic-web/docker-compose.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
services:
|
||||||
|
redis-api:
|
||||||
|
build:
|
||||||
|
context: ../redis-api
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
- REDIS_CONNECTION=redis:6379
|
||||||
|
- PRIMARY_API_KEY=
|
||||||
|
- SECONDARY_API_KEY=
|
||||||
|
- NODE_ENV=development
|
||||||
|
redis:
|
||||||
|
image: redis:6
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
redisinsight:
|
||||||
|
image: redis/redisinsight:latest
|
||||||
|
ports:
|
||||||
|
- "5540:5540"
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
12
apps/scandic-web/env/server.ts
vendored
12
apps/scandic-web/env/server.ts
vendored
@@ -185,6 +185,13 @@ export const env = createEnv({
|
|||||||
.number()
|
.number()
|
||||||
.default(10 * 60)
|
.default(10 * 60)
|
||||||
.transform((val) => (process.env.CMS_ENVIRONMENT === "test" ? 60 : val)),
|
.transform((val) => (process.env.CMS_ENVIRONMENT === "test" ? 60 : val)),
|
||||||
|
REDIS_API_HOST: z.string().optional(),
|
||||||
|
REDIS_API_KEY: z.string().optional(),
|
||||||
|
BRANCH:
|
||||||
|
process.env.NODE_ENV !== "development"
|
||||||
|
? z.string()
|
||||||
|
: z.string().optional().default("dev"),
|
||||||
|
GIT_SHA: z.string().optional(),
|
||||||
},
|
},
|
||||||
emptyStringAsUndefined: true,
|
emptyStringAsUndefined: true,
|
||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
@@ -273,6 +280,11 @@ export const env = createEnv({
|
|||||||
CACHE_TIME_HOTELDATA: process.env.CACHE_TIME_HOTELDATA,
|
CACHE_TIME_HOTELDATA: process.env.CACHE_TIME_HOTELDATA,
|
||||||
CACHE_TIME_HOTELS: process.env.CACHE_TIME_HOTELS,
|
CACHE_TIME_HOTELS: process.env.CACHE_TIME_HOTELS,
|
||||||
CACHE_TIME_CITY_SEARCH: process.env.CACHE_TIME_CITY_SEARCH,
|
CACHE_TIME_CITY_SEARCH: process.env.CACHE_TIME_CITY_SEARCH,
|
||||||
|
|
||||||
|
REDIS_API_HOST: process.env.REDIS_API_HOST,
|
||||||
|
REDIS_API_KEY: process.env.REDIS_API_KEY,
|
||||||
|
BRANCH: process.env.BRANCH,
|
||||||
|
GIT_SHA: process.env.GIT_SHA,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as Sentry from "@sentry/nextjs"
|
import * as Sentry from "@sentry/nextjs"
|
||||||
|
|
||||||
import { env } from "./env/server"
|
import { env } from "./env/server"
|
||||||
|
import { isEdge } from "./utils/isEdge"
|
||||||
|
|
||||||
export async function register() {
|
export async function register() {
|
||||||
/*
|
/*
|
||||||
@@ -45,12 +46,9 @@ async function configureApplicationInsights() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function configureSentry() {
|
async function configureSentry() {
|
||||||
switch (process.env.NEXT_RUNTIME) {
|
if (isEdge) {
|
||||||
case "edge": {
|
|
||||||
await import("./sentry.edge.config")
|
await import("./sentry.edge.config")
|
||||||
}
|
} else {
|
||||||
case "nodejs": {
|
|
||||||
await import("./sentry.server.config")
|
await import("./sentry.server.config")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,20 @@ import { request } from "./request"
|
|||||||
import type { BatchRequestDocument } from "graphql-request"
|
import type { BatchRequestDocument } from "graphql-request"
|
||||||
|
|
||||||
import type { Data } from "@/types/request"
|
import type { Data } from "@/types/request"
|
||||||
|
import type { CacheTime } from "@/services/dataCache"
|
||||||
|
|
||||||
export async function batchRequest<T>(
|
export async function batchRequest<T>(
|
||||||
queries: (BatchRequestDocument & { options?: RequestInit })[]
|
queries: (BatchRequestDocument & {
|
||||||
|
cacheOptions?: {
|
||||||
|
key: string | string[]
|
||||||
|
ttl: CacheTime
|
||||||
|
}
|
||||||
|
})[]
|
||||||
): Promise<Data<T>> {
|
): Promise<Data<T>> {
|
||||||
try {
|
try {
|
||||||
const response = await Promise.allSettled(
|
const response = await Promise.allSettled(
|
||||||
queries.map((query) =>
|
queries.map((query) =>
|
||||||
request<T>(query.document, query.variables, query.options)
|
request<T>(query.document, query.variables, query.cacheOptions)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import fetchRetry from "fetch-retry"
|
import fetchRetry from "fetch-retry"
|
||||||
import { GraphQLClient } from "graphql-request"
|
import { GraphQLClient } from "graphql-request"
|
||||||
import { cache } from "react"
|
import { cache as reactCache } from "react"
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { getPreviewHash, isPreviewByUid } from "@/lib/previewContext"
|
import { getPreviewHash, isPreviewByUid } from "@/lib/previewContext"
|
||||||
|
|
||||||
|
import { type CacheTime, getCacheClient } from "@/services/dataCache"
|
||||||
|
|
||||||
import { request as _request } from "./_request"
|
import { request as _request } from "./_request"
|
||||||
|
|
||||||
import type { DocumentNode } from "graphql"
|
import type { DocumentNode } from "graphql"
|
||||||
@@ -14,7 +16,28 @@ import type { Data } from "@/types/request"
|
|||||||
export async function request<T>(
|
export async function request<T>(
|
||||||
query: string | DocumentNode,
|
query: string | DocumentNode,
|
||||||
variables?: Record<string, any>,
|
variables?: Record<string, any>,
|
||||||
params?: RequestInit
|
cacheOptions?: {
|
||||||
|
key: string | string[]
|
||||||
|
ttl: CacheTime
|
||||||
|
}
|
||||||
|
): Promise<Data<T>> {
|
||||||
|
const doCall = () => internalRequest<T>(query, variables)
|
||||||
|
if (!cacheOptions) {
|
||||||
|
console.warn("[NO CACHE] for query", query)
|
||||||
|
return doCall()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey: string = Array.isArray(cacheOptions.key)
|
||||||
|
? cacheOptions.key.join("_")
|
||||||
|
: cacheOptions.key
|
||||||
|
|
||||||
|
const _dataCache = await getCacheClient()
|
||||||
|
return _dataCache.cacheOrGet(cacheKey, doCall, cacheOptions.ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
function internalRequest<T>(
|
||||||
|
query: string | DocumentNode,
|
||||||
|
variables?: Record<string, any>
|
||||||
): Promise<Data<T>> {
|
): Promise<Data<T>> {
|
||||||
const shouldUsePreview = variables?.uid
|
const shouldUsePreview = variables?.uid
|
||||||
? isPreviewByUid(variables.uid)
|
? isPreviewByUid(variables.uid)
|
||||||
@@ -24,7 +47,10 @@ export async function request<T>(
|
|||||||
|
|
||||||
// Creating a new client for each request to avoid conflicting parameters
|
// Creating a new client for each request to avoid conflicting parameters
|
||||||
const client = new GraphQLClient(cmsUrl, {
|
const client = new GraphQLClient(cmsUrl, {
|
||||||
fetch: cache(async function (url: URL | RequestInfo, params?: RequestInit) {
|
fetch: reactCache(async function (
|
||||||
|
url: URL | RequestInfo,
|
||||||
|
params?: RequestInit
|
||||||
|
) {
|
||||||
const wrappedFetch = fetchRetry(fetch, {
|
const wrappedFetch = fetchRetry(fetch, {
|
||||||
retries: 3,
|
retries: 3,
|
||||||
retryDelay: function (attempt) {
|
retryDelay: function (attempt) {
|
||||||
@@ -38,16 +64,12 @@ export async function request<T>(
|
|||||||
const mergedParams =
|
const mergedParams =
|
||||||
shouldUsePreview && previewHash
|
shouldUsePreview && previewHash
|
||||||
? {
|
? {
|
||||||
...params,
|
|
||||||
headers: {
|
headers: {
|
||||||
...params?.headers,
|
|
||||||
live_preview: previewHash,
|
live_preview: previewHash,
|
||||||
preview_token: env.CMS_PREVIEW_TOKEN,
|
preview_token: env.CMS_PREVIEW_TOKEN,
|
||||||
},
|
},
|
||||||
cache: undefined,
|
|
||||||
next: undefined,
|
|
||||||
}
|
}
|
||||||
: params
|
: {}
|
||||||
|
|
||||||
return _request(client, query, variables, mergedParams)
|
return _request(client, query, variables, mergedParams)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type NextMiddleware,NextResponse } from "next/server"
|
import { type NextMiddleware, NextResponse } from "next/server"
|
||||||
|
|
||||||
import { REDEMPTION, SEARCHTYPE } from "@/constants/booking"
|
import { REDEMPTION, SEARCHTYPE } from "@/constants/booking"
|
||||||
import { login } from "@/constants/routes/handleAuth"
|
import { login } from "@/constants/routes/handleAuth"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type NextMiddleware,NextResponse } from "next/server"
|
import { type NextMiddleware, NextResponse } from "next/server"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
|||||||
@@ -40,4 +40,4 @@ schedule = "@daily"
|
|||||||
[[headers]]
|
[[headers]]
|
||||||
for = "/_next/static/*"
|
for = "/_next/static/*"
|
||||||
[headers.values]
|
[headers.values]
|
||||||
cache-control = "public, max-age=31536001, immutable"
|
cache-control = "public, max-age=31536000, immutable"
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ jiti("./env/client")
|
|||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
env: {
|
||||||
|
BRANCH: process.env.BRANCH || "local",
|
||||||
|
GIT_SHA: process.env.COMMIT_REF || "",
|
||||||
|
},
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
eslint: { ignoreDuringBuilds: true },
|
eslint: { ignoreDuringBuilds: true },
|
||||||
trailingSlash: false,
|
trailingSlash: false,
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
"immer": "10.1.1",
|
"immer": "10.1.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
|
"ioredis": "^5.5.0",
|
||||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||||
"libphonenumber-js": "^1.10.60",
|
"libphonenumber-js": "^1.10.60",
|
||||||
"nanoid": "^5.0.9",
|
"nanoid": "^5.0.9",
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import type {
|
|||||||
GetAccountPageRefsSchema,
|
GetAccountPageRefsSchema,
|
||||||
GetAccountPageSchema,
|
GetAccountPageSchema,
|
||||||
} from "@/types/trpc/routers/contentstack/accountPage"
|
} from "@/types/trpc/routers/contentstack/accountPage"
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
const meter = metrics.getMeter("trpc.accountPage")
|
const meter = metrics.getMeter("trpc.accountPage")
|
||||||
|
|
||||||
@@ -64,10 +63,8 @@ export const accountPageQueryRouter = router({
|
|||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateRefsResponseTag(lang, uid),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateRefsResponseTag(lang, uid)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,10 +125,8 @@ export const accountPageQueryRouter = router({
|
|||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: tags,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { notFound } from "@/server/errors/trpc"
|
|||||||
import { contentstackBaseProcedure, router } from "@/server/trpc"
|
import { contentstackBaseProcedure, router } from "@/server/trpc"
|
||||||
import { langInput } from "@/server/utils"
|
import { langInput } from "@/server/utils"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
import {
|
import {
|
||||||
generateRefsResponseTag,
|
generateRefsResponseTag,
|
||||||
generateTag,
|
generateTag,
|
||||||
@@ -107,10 +108,8 @@ const getContactConfig = cache(async (lang: Lang) => {
|
|||||||
locale: lang,
|
locale: lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: `${lang}:contact`,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [`${lang}:contact`],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -176,10 +175,8 @@ export const baseQueryRouter = router({
|
|||||||
locale: lang,
|
locale: lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateRefsResponseTag(lang, "header"),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateRefsResponseTag(lang, "header")],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -244,7 +241,7 @@ export const baseQueryRouter = router({
|
|||||||
const response = await request<GetHeaderData>(
|
const response = await request<GetHeaderData>(
|
||||||
GetHeader,
|
GetHeader,
|
||||||
{ locale: lang },
|
{ locale: lang },
|
||||||
{ cache: "force-cache", next: { tags } }
|
{ key: tags, ttl: "max" }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
@@ -305,10 +302,8 @@ export const baseQueryRouter = router({
|
|||||||
locale: input.lang,
|
locale: input.lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateRefsResponseTag(input.lang, "current_header"),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateRefsResponseTag(input.lang, "current_header")],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
getCurrentHeaderCounter.add(1, { lang: input.lang })
|
getCurrentHeaderCounter.add(1, { lang: input.lang })
|
||||||
@@ -326,10 +321,8 @@ export const baseQueryRouter = router({
|
|||||||
GetCurrentHeader,
|
GetCurrentHeader,
|
||||||
{ locale: input.lang },
|
{ locale: input.lang },
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(input.lang, currentHeaderUID),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(input.lang, currentHeaderUID)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -397,10 +390,8 @@ export const baseQueryRouter = router({
|
|||||||
locale: input.lang,
|
locale: input.lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateRefsResponseTag(input.lang, "current_footer"),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateRefsResponseTag(input.lang, "current_footer")],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// There's currently no error handling/validation for the responseRef, should it be added?
|
// There's currently no error handling/validation for the responseRef, should it be added?
|
||||||
@@ -422,10 +413,8 @@ export const baseQueryRouter = router({
|
|||||||
locale: input.lang,
|
locale: input.lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(input.lang, currentFooterUID),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(input.lang, currentFooterUID)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -486,10 +475,8 @@ export const baseQueryRouter = router({
|
|||||||
locale: lang,
|
locale: lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateRefsResponseTag(lang, "footer"),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateRefsResponseTag(lang, "footer")],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -563,10 +550,8 @@ export const baseQueryRouter = router({
|
|||||||
locale: lang,
|
locale: lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: tags,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -620,7 +605,10 @@ export const baseQueryRouter = router({
|
|||||||
.input(langInput)
|
.input(langInput)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const lang = input.lang ?? ctx.lang
|
const lang = input.lang ?? ctx.lang
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
generateRefsResponseTag(lang, "site_config", "root"),
|
||||||
|
async () => {
|
||||||
getSiteConfigRefCounter.add(1, { lang })
|
getSiteConfigRefCounter.add(1, { lang })
|
||||||
console.info(
|
console.info(
|
||||||
"contentstack.siteConfig.ref start",
|
"contentstack.siteConfig.ref start",
|
||||||
@@ -632,10 +620,8 @@ export const baseQueryRouter = router({
|
|||||||
locale: lang,
|
locale: lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateRefsResponseTag(lang, "site_config"),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateRefsResponseTag(lang, "site_config")],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -680,8 +666,11 @@ export const baseQueryRouter = router({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const connections = getSiteConfigConnections(validatedSiteConfigRef.data)
|
const connections = getSiteConfigConnections(
|
||||||
const siteConfigUid = responseRef.data.all_site_config.items[0].system.uid
|
validatedSiteConfigRef.data
|
||||||
|
)
|
||||||
|
const siteConfigUid =
|
||||||
|
responseRef.data.all_site_config.items[0].system.uid
|
||||||
|
|
||||||
const tags = [
|
const tags = [
|
||||||
generateTagsFromSystem(lang, connections),
|
generateTagsFromSystem(lang, connections),
|
||||||
@@ -706,8 +695,8 @@ export const baseQueryRouter = router({
|
|||||||
locale: lang,
|
locale: lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: tags,
|
||||||
next: { tags },
|
ttl: "max",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
getContactConfig(lang),
|
getContactConfig(lang),
|
||||||
@@ -772,5 +761,8 @@ export const baseQueryRouter = router({
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"max"
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import { request } from "@/lib/graphql/request"
|
|||||||
import { notFound } from "@/server/errors/trpc"
|
import { notFound } from "@/server/errors/trpc"
|
||||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||||
|
|
||||||
|
import { generateRefsResponseTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
import { breadcrumbsRefsSchema, breadcrumbsSchema } from "./output"
|
import { breadcrumbsRefsSchema, breadcrumbsSchema } from "./output"
|
||||||
import { getTags } from "./utils"
|
import { getTags } from "./utils"
|
||||||
|
|
||||||
@@ -46,7 +48,6 @@ import type {
|
|||||||
RawBreadcrumbsSchema,
|
RawBreadcrumbsSchema,
|
||||||
} from "@/types/trpc/routers/contentstack/breadcrumbs"
|
} from "@/types/trpc/routers/contentstack/breadcrumbs"
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
import { generateRefsResponseTag } from "@/utils/generateTag"
|
|
||||||
|
|
||||||
const meter = metrics.getMeter("trpc.breadcrumbs")
|
const meter = metrics.getMeter("trpc.breadcrumbs")
|
||||||
|
|
||||||
@@ -89,8 +90,8 @@ const getBreadcrumbs = cache(async function fetchMemoizedBreadcrumbs<T>(
|
|||||||
refQuery,
|
refQuery,
|
||||||
{ locale: lang, uid },
|
{ locale: lang, uid },
|
||||||
{
|
{
|
||||||
cache: `force-cache`,
|
key: generateRefsResponseTag(lang, uid, "breadcrumbs"),
|
||||||
next: { tags: [generateRefsResponseTag(lang, uid)] },
|
ttl: "max",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,8 +130,8 @@ const getBreadcrumbs = cache(async function fetchMemoizedBreadcrumbs<T>(
|
|||||||
query,
|
query,
|
||||||
{ locale: lang, uid },
|
{ locale: lang, uid },
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: tags,
|
||||||
next: { tags },
|
ttl: "max",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
type TrackingSDKPageData,
|
type TrackingSDKPageData,
|
||||||
} from "@/types/components/tracking"
|
} from "@/types/components/tracking"
|
||||||
import type { GetCollectionPageSchema } from "@/types/trpc/routers/contentstack/collectionPage"
|
import type { GetCollectionPageSchema } from "@/types/trpc/routers/contentstack/collectionPage"
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
export const collectionPageQueryRouter = router({
|
export const collectionPageQueryRouter = router({
|
||||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
||||||
@@ -45,10 +44,8 @@ export const collectionPageQueryRouter = router({
|
|||||||
GetCollectionPage,
|
GetCollectionPage,
|
||||||
{ locale: lang, uid },
|
{ locale: lang, uid },
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: tags,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { metrics } from "@opentelemetry/api"
|
import { metrics } from "@opentelemetry/api"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import { GetCollectionPageRefs } from "@/lib/graphql/Query/CollectionPage/CollectionPage.graphql"
|
import { GetCollectionPageRefs } from "@/lib/graphql/Query/CollectionPage/CollectionPage.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import { notFound } from "@/server/errors/trpc"
|
import { notFound } from "@/server/errors/trpc"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
||||||
|
|
||||||
import { collectionPageRefsSchema } from "./output"
|
import { collectionPageRefsSchema } from "./output"
|
||||||
|
|
||||||
import { CollectionPageEnum } from "@/types/enums/collectionPage"
|
import { CollectionPageEnum } from "@/types/enums/collectionPage"
|
||||||
import { System } from "@/types/requests/system"
|
import type { System } from "@/types/requests/system"
|
||||||
import {
|
import type {
|
||||||
CollectionPageRefs,
|
CollectionPageRefs,
|
||||||
GetCollectionPageRefsSchema,
|
GetCollectionPageRefsSchema,
|
||||||
} from "@/types/trpc/routers/contentstack/collectionPage"
|
} from "@/types/trpc/routers/contentstack/collectionPage"
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
const meter = metrics.getMeter("trpc.collectionPage")
|
const meter = metrics.getMeter("trpc.collectionPage")
|
||||||
// OpenTelemetry metrics: CollectionPage
|
// OpenTelemetry metrics: CollectionPage
|
||||||
@@ -41,15 +42,17 @@ export async function fetchCollectionPageRefs(lang: Lang, uid: string) {
|
|||||||
query: { lang, uid },
|
query: { lang, uid },
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const refsResponse = await request<GetCollectionPageRefsSchema>(
|
|
||||||
GetCollectionPageRefs,
|
const cacheClient = await getCacheClient()
|
||||||
{ locale: lang, uid },
|
const cacheKey = generateTag(lang, uid)
|
||||||
{
|
const refsResponse = await cacheClient.cacheOrGet(
|
||||||
cache: "force-cache",
|
cacheKey,
|
||||||
next: {
|
async () =>
|
||||||
tags: [generateTag(lang, uid)],
|
await request<GetCollectionPageRefsSchema>(GetCollectionPageRefs, {
|
||||||
},
|
locale: lang,
|
||||||
}
|
uid,
|
||||||
|
}),
|
||||||
|
"max"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!refsResponse.data) {
|
if (!refsResponse.data) {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
|
|
||||||
import type { TrackingSDKPageData } from "@/types/components/tracking"
|
import type { TrackingSDKPageData } from "@/types/components/tracking"
|
||||||
import type { GetContentPageSchema } from "@/types/trpc/routers/contentstack/contentPage"
|
import type { GetContentPageSchema } from "@/types/trpc/routers/contentstack/contentPage"
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
export const contentPageQueryRouter = router({
|
export const contentPageQueryRouter = router({
|
||||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
||||||
@@ -43,33 +42,27 @@ export const contentPageQueryRouter = router({
|
|||||||
{
|
{
|
||||||
document: GetContentPage,
|
document: GetContentPage,
|
||||||
variables: { locale: lang, uid },
|
variables: { locale: lang, uid },
|
||||||
options: {
|
cacheOptions: {
|
||||||
cache: "force-cache",
|
key: `${tags.join(",")}:contentPage`,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
document: GetContentPageBlocksBatch1,
|
document: GetContentPageBlocksBatch1,
|
||||||
variables: { locale: lang, uid },
|
variables: { locale: lang, uid },
|
||||||
options: {
|
cacheOptions: {
|
||||||
cache: "force-cache",
|
key: `${tags.join(",")}:contentPageBlocksBatch1`,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
document: GetContentPageBlocksBatch2,
|
document: GetContentPageBlocksBatch2,
|
||||||
variables: { locale: lang, uid },
|
variables: { locale: lang, uid },
|
||||||
options: {
|
cacheOptions: {
|
||||||
cache: "force-cache",
|
key: `${tags.join(",")}:contentPageBlocksBatch2`,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -49,21 +49,17 @@ export async function fetchContentPageRefs(lang: Lang, uid: string) {
|
|||||||
{
|
{
|
||||||
document: GetContentPageRefs,
|
document: GetContentPageRefs,
|
||||||
variables: { locale: lang, uid },
|
variables: { locale: lang, uid },
|
||||||
options: {
|
cacheOptions: {
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid)],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
document: GetContentPageBlocksRefs,
|
document: GetContentPageBlocksRefs,
|
||||||
variables: { locale: lang, uid },
|
variables: { locale: lang, uid },
|
||||||
options: {
|
cacheOptions: {
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid + 1),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid + 1)],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -46,10 +46,8 @@ export const destinationCityPageQueryRouter = router({
|
|||||||
GetDestinationCityPageRefs,
|
GetDestinationCityPageRefs,
|
||||||
{ locale: lang, uid },
|
{ locale: lang, uid },
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,10 +107,8 @@ export const destinationCityPageQueryRouter = router({
|
|||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: tags,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
@@ -153,7 +149,11 @@ export const destinationCityPageQueryRouter = router({
|
|||||||
}
|
}
|
||||||
const destinationCityPage = validatedResponse.data.destination_city_page
|
const destinationCityPage = validatedResponse.data.destination_city_page
|
||||||
const cityIdentifier = destinationCityPage.destination_settings.city
|
const cityIdentifier = destinationCityPage.destination_settings.city
|
||||||
const city = await getCityByCityIdentifier(cityIdentifier, serviceToken)
|
const city = await getCityByCityIdentifier({
|
||||||
|
cityIdentifier,
|
||||||
|
lang,
|
||||||
|
serviceToken,
|
||||||
|
})
|
||||||
|
|
||||||
if (!city) {
|
if (!city) {
|
||||||
getDestinationCityPageFailCounter.add(1, {
|
getDestinationCityPageFailCounter.add(1, {
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
getCityPageUrlsSuccessCounter,
|
getCityPageUrlsSuccessCounter,
|
||||||
} from "./telemetry"
|
} from "./telemetry"
|
||||||
|
|
||||||
import type { BatchRequestDocument } from "graphql-request"
|
|
||||||
|
|
||||||
import { DestinationCityPageEnum } from "@/types/enums/destinationCityPage"
|
import { DestinationCityPageEnum } from "@/types/enums/destinationCityPage"
|
||||||
import type { System } from "@/types/requests/system"
|
import type { System } from "@/types/requests/system"
|
||||||
import type {
|
import type {
|
||||||
@@ -78,17 +76,15 @@ export async function getCityPageCount(lang: Lang) {
|
|||||||
"contentstack.cityPageCount start",
|
"contentstack.cityPageCount start",
|
||||||
JSON.stringify({ query: { lang } })
|
JSON.stringify({ query: { lang } })
|
||||||
)
|
)
|
||||||
const tags = [`${lang}:city_page_count`]
|
|
||||||
const response = await request<GetCityPageCountData>(
|
const response = await request<GetCityPageCountData>(
|
||||||
GetCityPageCount,
|
GetCityPageCount,
|
||||||
{
|
{
|
||||||
locale: lang,
|
locale: lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: `${lang}:city_page_count`,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
@@ -148,21 +144,14 @@ export async function getCityPageUrls(lang: Lang) {
|
|||||||
// The `batchRequest` function is not working here, because the arrayMerge is
|
// The `batchRequest` function is not working here, because the arrayMerge is
|
||||||
// used for other purposes.
|
// used for other purposes.
|
||||||
const amountOfRequests = Math.ceil(count / 100)
|
const amountOfRequests = Math.ceil(count / 100)
|
||||||
const requests: (BatchRequestDocument & { options?: RequestInit })[] =
|
|
||||||
Array.from({ length: amountOfRequests }).map((_, i) => ({
|
|
||||||
document: GetCityPageUrls,
|
|
||||||
variables: { locale: lang, skip: i * 100 },
|
|
||||||
options: {
|
|
||||||
cache: "force-cache",
|
|
||||||
next: {
|
|
||||||
tags: [`${lang}:city_page_urls_batch_${i}`],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const batchedResponse = await Promise.all(
|
const batchedResponse = await Promise.all(
|
||||||
requests.map((req) =>
|
Array.from({ length: amountOfRequests }).map((_, i) =>
|
||||||
request<GetCityPageUrlsData>(req.document, req.variables, req.options)
|
request<GetCityPageUrlsData>(
|
||||||
|
GetCityPageUrls,
|
||||||
|
{ locale: lang, skip: i * 100 },
|
||||||
|
{ key: `${lang}:city_page_urls_batch_${i}`, ttl: "max" }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
const validatedResponse = batchedCityPageUrlsSchema.safeParse(batchedResponse)
|
const validatedResponse = batchedCityPageUrlsSchema.safeParse(batchedResponse)
|
||||||
|
|||||||
@@ -51,10 +51,8 @@ export const destinationCountryPageQueryRouter = router({
|
|||||||
GetDestinationCountryPageRefs,
|
GetDestinationCountryPageRefs,
|
||||||
{ locale: lang, uid },
|
{ locale: lang, uid },
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,10 +112,8 @@ export const destinationCountryPageQueryRouter = router({
|
|||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: tags,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { env } from "@/env/server"
|
|
||||||
import { GetDestinationCityListData } from "@/lib/graphql/Query/DestinationCityPage/DestinationCityListData.graphql"
|
import { GetDestinationCityListData } from "@/lib/graphql/Query/DestinationCityPage/DestinationCityListData.graphql"
|
||||||
import { GetCountryPageUrls } from "@/lib/graphql/Query/DestinationCountryPage/DestinationCountryPageUrl.graphql"
|
import { GetCountryPageUrls } from "@/lib/graphql/Query/DestinationCountryPage/DestinationCountryPageUrl.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import { toApiLang } from "@/server/utils"
|
|
||||||
|
|
||||||
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
||||||
|
|
||||||
@@ -20,7 +18,6 @@ import {
|
|||||||
|
|
||||||
import { ApiCountry, type Country } from "@/types/enums/country"
|
import { ApiCountry, type Country } from "@/types/enums/country"
|
||||||
import { DestinationCountryPageEnum } from "@/types/enums/destinationCountryPage"
|
import { DestinationCountryPageEnum } from "@/types/enums/destinationCountryPage"
|
||||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
|
||||||
import type { System } from "@/types/requests/system"
|
import type { System } from "@/types/requests/system"
|
||||||
import type { GetDestinationCityListDataResponse } from "@/types/trpc/routers/contentstack/destinationCityPage"
|
import type { GetDestinationCityListDataResponse } from "@/types/trpc/routers/contentstack/destinationCityPage"
|
||||||
import type {
|
import type {
|
||||||
@@ -85,7 +82,7 @@ export async function getCityListDataByCityIdentifier(
|
|||||||
"contentstack.cityListData start",
|
"contentstack.cityListData start",
|
||||||
JSON.stringify({ query: { lang, cityIdentifier } })
|
JSON.stringify({ query: { lang, cityIdentifier } })
|
||||||
)
|
)
|
||||||
const tag = `${lang}:city_list_data:${cityIdentifier}`
|
|
||||||
const response = await request<GetDestinationCityListDataResponse>(
|
const response = await request<GetDestinationCityListDataResponse>(
|
||||||
GetDestinationCityListData,
|
GetDestinationCityListData,
|
||||||
{
|
{
|
||||||
@@ -93,10 +90,8 @@ export async function getCityListDataByCityIdentifier(
|
|||||||
cityIdentifier,
|
cityIdentifier,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: `${lang}:city_list_data:${cityIdentifier}`,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [tag],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -148,23 +143,12 @@ export async function getCityPages(
|
|||||||
serviceToken: string,
|
serviceToken: string,
|
||||||
country: Country
|
country: Country
|
||||||
) {
|
) {
|
||||||
const apiLang = toApiLang(lang)
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
language: apiLang,
|
|
||||||
})
|
|
||||||
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 apiCountry = ApiCountry[lang][country]
|
const apiCountry = ApiCountry[lang][country]
|
||||||
const cities = await getCitiesByCountry([apiCountry], options, params, lang)
|
const cities = await getCitiesByCountry({
|
||||||
|
countries: [apiCountry],
|
||||||
|
lang,
|
||||||
|
serviceToken,
|
||||||
|
})
|
||||||
|
|
||||||
const publishedCities = cities[apiCountry].filter((city) => city.isPublished)
|
const publishedCities = cities[apiCountry].filter((city) => city.isPublished)
|
||||||
|
|
||||||
@@ -201,10 +185,8 @@ export async function getCountryPageUrls(lang: Lang) {
|
|||||||
locale: lang,
|
locale: lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: tag,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [tag],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { env } from "@/env/server"
|
|
||||||
import {
|
import {
|
||||||
GetDestinationOverviewPage,
|
GetDestinationOverviewPage,
|
||||||
GetDestinationOverviewPageRefs,
|
GetDestinationOverviewPageRefs,
|
||||||
@@ -10,9 +9,9 @@ import {
|
|||||||
router,
|
router,
|
||||||
serviceProcedure,
|
serviceProcedure,
|
||||||
} from "@/server/trpc"
|
} from "@/server/trpc"
|
||||||
import { toApiLang } from "@/server/utils"
|
|
||||||
|
|
||||||
import { generateTag } from "@/utils/generateTag"
|
import { generateTag } from "@/utils/generateTag"
|
||||||
|
import { safeTry } from "@/utils/safeTry"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCitiesByCountry,
|
getCitiesByCountry,
|
||||||
@@ -42,7 +41,6 @@ import {
|
|||||||
TrackingChannelEnum,
|
TrackingChannelEnum,
|
||||||
type TrackingSDKPageData,
|
type TrackingSDKPageData,
|
||||||
} from "@/types/components/tracking"
|
} from "@/types/components/tracking"
|
||||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
|
||||||
import type {
|
import type {
|
||||||
GetDestinationOverviewPageData,
|
GetDestinationOverviewPageData,
|
||||||
GetDestinationOverviewPageRefsSchema,
|
GetDestinationOverviewPageRefsSchema,
|
||||||
@@ -66,10 +64,8 @@ export const destinationOverviewPageQueryRouter = router({
|
|||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!refsResponse.data) {
|
if (!refsResponse.data) {
|
||||||
@@ -133,10 +129,8 @@ export const destinationOverviewPageQueryRouter = router({
|
|||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
@@ -207,23 +201,11 @@ export const destinationOverviewPageQueryRouter = router({
|
|||||||
}),
|
}),
|
||||||
destinations: router({
|
destinations: router({
|
||||||
get: serviceProcedure.query(async function ({ ctx }) {
|
get: serviceProcedure.query(async function ({ ctx }) {
|
||||||
const apiLang = toApiLang(ctx.lang)
|
const countries = await getCountries({
|
||||||
const params = new URLSearchParams({
|
lang: ctx.lang,
|
||||||
language: apiLang,
|
serviceToken: ctx.serviceToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
const options: RequestOptionsWithOutBody = {
|
|
||||||
// needs to clear default option as only
|
|
||||||
// cache or next.revalidate is permitted
|
|
||||||
cache: undefined,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
||||||
},
|
|
||||||
next: {
|
|
||||||
revalidate: env.CACHE_TIME_HOTELS,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const countries = await getCountries(options, params, ctx.lang)
|
|
||||||
const countryPages = await getCountryPageUrls(ctx.lang)
|
const countryPages = await getCountryPageUrls(ctx.lang)
|
||||||
|
|
||||||
if (!countries) {
|
if (!countries) {
|
||||||
@@ -232,13 +214,12 @@ export const destinationOverviewPageQueryRouter = router({
|
|||||||
|
|
||||||
const countryNames = countries.data.map((country) => country.name)
|
const countryNames = countries.data.map((country) => country.name)
|
||||||
|
|
||||||
const citiesByCountry = await getCitiesByCountry(
|
const citiesByCountry = await getCitiesByCountry({
|
||||||
countryNames,
|
lang: ctx.lang,
|
||||||
options,
|
countries: countryNames,
|
||||||
params,
|
serviceToken: ctx.serviceToken,
|
||||||
ctx.lang,
|
onlyPublished: true,
|
||||||
true
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const cityPages = await getCityPageUrls(ctx.lang)
|
const cityPages = await getCityPageUrls(ctx.lang)
|
||||||
|
|
||||||
@@ -246,15 +227,11 @@ export const destinationOverviewPageQueryRouter = router({
|
|||||||
Object.entries(citiesByCountry).map(async ([country, cities]) => {
|
Object.entries(citiesByCountry).map(async ([country, cities]) => {
|
||||||
const citiesWithHotelCount = await Promise.all(
|
const citiesWithHotelCount = await Promise.all(
|
||||||
cities.map(async (city) => {
|
cities.map(async (city) => {
|
||||||
const hotelIdsParams = new URLSearchParams({
|
const [hotels] = await safeTry(
|
||||||
language: apiLang,
|
getHotelIdsByCityId({
|
||||||
city: city.id,
|
cityId: city.id,
|
||||||
|
serviceToken: ctx.serviceToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
const hotels = await getHotelIdsByCityId(
|
|
||||||
city.id,
|
|
||||||
options,
|
|
||||||
hotelIdsParams
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const cityPage = cityPages.find(
|
const cityPage = cityPages.find(
|
||||||
@@ -268,7 +245,7 @@ export const destinationOverviewPageQueryRouter = router({
|
|||||||
return {
|
return {
|
||||||
id: city.id,
|
id: city.id,
|
||||||
name: city.name,
|
name: city.name,
|
||||||
hotelIds: hotels,
|
hotelIds: hotels || [],
|
||||||
hotelCount: hotels?.length ?? 0,
|
hotelCount: hotels?.length ?? 0,
|
||||||
url: cityPage.url,
|
url: cityPage.url,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,8 @@ export const hotelPageQueryRouter = router({
|
|||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import {
|
|||||||
getHotelPageUrlsSuccessCounter,
|
getHotelPageUrlsSuccessCounter,
|
||||||
} from "./telemetry"
|
} from "./telemetry"
|
||||||
|
|
||||||
import type { BatchRequestDocument } from "graphql-request"
|
|
||||||
|
|
||||||
import { HotelPageEnum } from "@/types/enums/hotelPage"
|
import { HotelPageEnum } from "@/types/enums/hotelPage"
|
||||||
import type { System } from "@/types/requests/system"
|
import type { System } from "@/types/requests/system"
|
||||||
import type {
|
import type {
|
||||||
@@ -48,10 +46,8 @@ export async function fetchHotelPageRefs(lang: Lang, uid: string) {
|
|||||||
GetHotelPageRefs,
|
GetHotelPageRefs,
|
||||||
{ locale: lang, uid },
|
{ locale: lang, uid },
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!refsResponse.data) {
|
if (!refsResponse.data) {
|
||||||
@@ -149,17 +145,14 @@ export async function getHotelPageCount(lang: Lang) {
|
|||||||
"contentstack.hotelPageCount start",
|
"contentstack.hotelPageCount start",
|
||||||
JSON.stringify({ query: { lang } })
|
JSON.stringify({ query: { lang } })
|
||||||
)
|
)
|
||||||
const tags = [`${lang}:hotel_page_count`]
|
|
||||||
const response = await request<GetHotelPageCountData>(
|
const response = await request<GetHotelPageCountData>(
|
||||||
GetHotelPageCount,
|
GetHotelPageCount,
|
||||||
{
|
{
|
||||||
locale: lang,
|
locale: lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: `${lang}:hotel_page_count`,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -220,21 +213,18 @@ export async function getHotelPageUrls(lang: Lang) {
|
|||||||
// The `batchRequest` function is not working here, because the arrayMerge is
|
// The `batchRequest` function is not working here, because the arrayMerge is
|
||||||
// used for other purposes.
|
// used for other purposes.
|
||||||
const amountOfRequests = Math.ceil(count / 100)
|
const amountOfRequests = Math.ceil(count / 100)
|
||||||
const requests: (BatchRequestDocument & { options?: RequestInit })[] =
|
const requests = Array.from({ length: amountOfRequests }).map((_, i) => ({
|
||||||
Array.from({ length: amountOfRequests }).map((_, i) => ({
|
|
||||||
document: GetHotelPageUrls,
|
document: GetHotelPageUrls,
|
||||||
variables: { locale: lang, skip: i * 100 },
|
variables: { locale: lang, skip: i * 100 },
|
||||||
options: {
|
cacheKey: `${lang}:hotel_page_urls_batch_${i}`,
|
||||||
cache: "force-cache",
|
|
||||||
next: {
|
|
||||||
tags: [`${lang}:hotel_page_urls_batch_${i}`],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const batchedResponse = await Promise.all(
|
const batchedResponse = await Promise.all(
|
||||||
requests.map((req) =>
|
requests.map((req) =>
|
||||||
request<GetHotelPageUrlsData>(req.document, req.variables, req.options)
|
request<GetHotelPageUrlsData>(req.document, req.variables, {
|
||||||
|
key: req.cacheKey,
|
||||||
|
ttl: "max",
|
||||||
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -149,21 +149,17 @@ export async function getUrlsOfAllLanguages(
|
|||||||
{
|
{
|
||||||
document: daDeEnDocument,
|
document: daDeEnDocument,
|
||||||
variables,
|
variables,
|
||||||
options: {
|
cacheOptions: {
|
||||||
cache: "force-cache",
|
ttl: "max",
|
||||||
next: {
|
key: tagsDaDeEn,
|
||||||
tags: tagsDaDeEn,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
document: fiNoSvDocument,
|
document: fiNoSvDocument,
|
||||||
variables,
|
variables,
|
||||||
options: {
|
cacheOptions: {
|
||||||
cache: "force-cache",
|
ttl: "max",
|
||||||
next: {
|
key: tagsFiNoSv,
|
||||||
tags: tagsFiNoSv,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const getAllLoyaltyLevels = cache(async (ctx: Context) => {
|
|||||||
const loyaltyLevelsConfigResponse = await request<LoyaltyLevelsResponse>(
|
const loyaltyLevelsConfigResponse = await request<LoyaltyLevelsResponse>(
|
||||||
GetAllLoyaltyLevels,
|
GetAllLoyaltyLevels,
|
||||||
{ lang: ctx.lang, level_ids: allLevelIds },
|
{ lang: ctx.lang, level_ids: allLevelIds },
|
||||||
{ next: { tags }, cache: "force-cache" }
|
{ key: tags, ttl: "max" }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!loyaltyLevelsConfigResponse.data) {
|
if (!loyaltyLevelsConfigResponse.data) {
|
||||||
@@ -113,10 +113,8 @@ export const getLoyaltyLevel = cache(
|
|||||||
GetLoyaltyLevel,
|
GetLoyaltyLevel,
|
||||||
{ lang: ctx.lang, level_id },
|
{ lang: ctx.lang, level_id },
|
||||||
{
|
{
|
||||||
next: {
|
key: generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id),
|
||||||
tags: [generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id)],
|
ttl: "max",
|
||||||
},
|
|
||||||
cache: "force-cache",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import type {
|
|||||||
GetLoyaltyPageRefsSchema,
|
GetLoyaltyPageRefsSchema,
|
||||||
GetLoyaltyPageSchema,
|
GetLoyaltyPageSchema,
|
||||||
} from "@/types/trpc/routers/contentstack/loyaltyPage"
|
} from "@/types/trpc/routers/contentstack/loyaltyPage"
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
const meter = metrics.getMeter("trpc.loyaltyPage")
|
const meter = metrics.getMeter("trpc.loyaltyPage")
|
||||||
// OpenTelemetry metrics: LoyaltyPage
|
// OpenTelemetry metrics: LoyaltyPage
|
||||||
@@ -64,10 +63,8 @@ export const loyaltyPageQueryRouter = router({
|
|||||||
GetLoyaltyPageRefs,
|
GetLoyaltyPageRefs,
|
||||||
variables,
|
variables,
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateRefsResponseTag(lang, uid),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateRefsResponseTag(lang, uid)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,8 +130,8 @@ export const loyaltyPageQueryRouter = router({
|
|||||||
GetLoyaltyPage,
|
GetLoyaltyPage,
|
||||||
variables,
|
variables,
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: tags,
|
||||||
next: { tags },
|
ttl: "max",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -64,10 +64,8 @@ const fetchMetadata = cache(async function fetchMemoizedMetadata<T>(
|
|||||||
query,
|
query,
|
||||||
{ locale: lang, uid },
|
{ locale: lang, uid },
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid, affix),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid, affix)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ApiLang, type Lang } from "@/constants/languages"
|
import { type Lang } from "@/constants/languages"
|
||||||
import { env } from "@/env/server"
|
|
||||||
import { getFiltersFromHotels } from "@/stores/destination-data/helper"
|
import { getFiltersFromHotels } from "@/stores/destination-data/helper"
|
||||||
|
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
@@ -12,7 +11,6 @@ import {
|
|||||||
} from "../../hotels/utils"
|
} from "../../hotels/utils"
|
||||||
|
|
||||||
import { ApiCountry } from "@/types/enums/country"
|
import { ApiCountry } from "@/types/enums/country"
|
||||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
|
||||||
import { RTETypeEnum } from "@/types/rte/enums"
|
import { RTETypeEnum } from "@/types/rte/enums"
|
||||||
import type {
|
import type {
|
||||||
MetadataInputSchema,
|
MetadataInputSchema,
|
||||||
@@ -218,17 +216,19 @@ export async function getCityData(
|
|||||||
const cityIdentifier = cities[0]
|
const cityIdentifier = cities[0]
|
||||||
|
|
||||||
if (cityIdentifier) {
|
if (cityIdentifier) {
|
||||||
const cityData = await getCityByCityIdentifier(
|
const cityData = await getCityByCityIdentifier({
|
||||||
cityIdentifier,
|
cityIdentifier,
|
||||||
serviceToken
|
serviceToken,
|
||||||
)
|
lang,
|
||||||
|
})
|
||||||
const hotelIds = await getHotelIdsByCityIdentifier(
|
const hotelIds = await getHotelIdsByCityIdentifier(
|
||||||
cityIdentifier,
|
cityIdentifier,
|
||||||
serviceToken
|
serviceToken
|
||||||
)
|
)
|
||||||
const hotels = await getHotelsByHotelIds(hotelIds, lang, serviceToken)
|
|
||||||
let filterType
|
|
||||||
|
|
||||||
|
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
||||||
|
|
||||||
|
let filterType
|
||||||
if (filter) {
|
if (filter) {
|
||||||
const allFilters = getFiltersFromHotels(hotels)
|
const allFilters = getFiltersFromHotels(hotels)
|
||||||
const facilityFilter = allFilters.facilityFilters.find(
|
const facilityFilter = allFilters.facilityFilters.find(
|
||||||
@@ -264,28 +264,12 @@ export async function getCountryData(
|
|||||||
const translatedCountry = ApiCountry[lang][country]
|
const translatedCountry = ApiCountry[lang][country]
|
||||||
let filterType
|
let filterType
|
||||||
|
|
||||||
const options: RequestOptionsWithOutBody = {
|
const hotelIds = await getHotelIdsByCountry({
|
||||||
// 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 hotelIdsParams = new URLSearchParams({
|
|
||||||
language: ApiLang.En,
|
|
||||||
country,
|
country,
|
||||||
|
serviceToken,
|
||||||
})
|
})
|
||||||
const hotelIds = await getHotelIdsByCountry(
|
|
||||||
country,
|
|
||||||
options,
|
|
||||||
hotelIdsParams
|
|
||||||
)
|
|
||||||
|
|
||||||
const hotels = await getHotelsByHotelIds(hotelIds, lang, serviceToken)
|
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
const allFilters = getFiltersFromHotels(hotels)
|
const allFilters = getFiltersFromHotels(hotels)
|
||||||
|
|||||||
@@ -84,10 +84,8 @@ export const pageSettingsQueryRouter = router({
|
|||||||
locale: lang,
|
locale: lang,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid, affix),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid, affix)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,8 @@ export const getSasTierComparison = cache(async (ctx: Context) => {
|
|||||||
GetAllSasTierComparison,
|
GetAllSasTierComparison,
|
||||||
{ lang: ctx.lang },
|
{ lang: ctx.lang },
|
||||||
{
|
{
|
||||||
next: {
|
key: tag,
|
||||||
tags: [tag],
|
ttl: "max",
|
||||||
},
|
|
||||||
cache: "force-cache",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
} from "@/server/trpc"
|
} from "@/server/trpc"
|
||||||
import { langInput } from "@/server/utils"
|
import { langInput } from "@/server/utils"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
|
|
||||||
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
|
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
|
||||||
import {
|
import {
|
||||||
rewardsAllInput,
|
rewardsAllInput,
|
||||||
@@ -46,8 +48,6 @@ import {
|
|||||||
getUnwrapSurpriseSuccessCounter,
|
getUnwrapSurpriseSuccessCounter,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
|
|
||||||
const ONE_HOUR = 60 * 60
|
|
||||||
|
|
||||||
export const rewardQueryRouter = router({
|
export const rewardQueryRouter = router({
|
||||||
all: contentStackBaseWithServiceProcedure
|
all: contentStackBaseWithServiceProcedure
|
||||||
.input(rewardsAllInput)
|
.input(rewardsAllInput)
|
||||||
@@ -174,12 +174,15 @@ export const rewardQueryRouter = router({
|
|||||||
? api.endpoints.v1.Profile.Reward.reward
|
? api.endpoints.v1.Profile.Reward.reward
|
||||||
: api.endpoints.v1.Profile.reward
|
: api.endpoints.v1.Profile.reward
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
return cacheClient.cacheOrGet(
|
||||||
|
endpoint,
|
||||||
|
async () => {
|
||||||
const apiResponse = await api.get(endpoint, {
|
const apiResponse = await api.get(endpoint, {
|
||||||
cache: undefined, // override defaultOptions
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||||
},
|
},
|
||||||
next: { revalidate: ONE_HOUR },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
@@ -292,13 +295,18 @@ export const rewardQueryRouter = router({
|
|||||||
: [],
|
: [],
|
||||||
couponCode: firstRedeemableCouponToExpire,
|
couponCode: firstRedeemableCouponToExpire,
|
||||||
coupons:
|
coupons:
|
||||||
apiReward && "coupon" in apiReward ? apiReward.coupon || [] : [],
|
apiReward && "coupon" in apiReward
|
||||||
|
? apiReward.coupon || []
|
||||||
|
: [],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
getCurrentRewardSuccessCounter.add(1)
|
getCurrentRewardSuccessCounter.add(1)
|
||||||
|
|
||||||
return { rewards }
|
return { rewards }
|
||||||
|
},
|
||||||
|
"1h"
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
surprises: contentStackBaseWithProtectedProcedure
|
surprises: contentStackBaseWithProtectedProcedure
|
||||||
.input(langInput.optional()) // lang is required for client, but not for server
|
.input(langInput.optional()) // lang is required for client, but not for server
|
||||||
@@ -310,12 +318,15 @@ export const rewardQueryRouter = router({
|
|||||||
? api.endpoints.v1.Profile.Reward.reward
|
? api.endpoints.v1.Profile.Reward.reward
|
||||||
: api.endpoints.v1.Profile.reward
|
: api.endpoints.v1.Profile.reward
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
endpoint,
|
||||||
|
async () => {
|
||||||
const apiResponse = await api.get(endpoint, {
|
const apiResponse = await api.get(endpoint, {
|
||||||
cache: undefined,
|
cache: undefined,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||||
},
|
},
|
||||||
next: { revalidate: ONE_HOUR },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
@@ -410,14 +421,17 @@ export const rewardQueryRouter = router({
|
|||||||
rewardType: surprise.rewardType,
|
rewardType: surprise.rewardType,
|
||||||
rewardTierLevel: undefined,
|
rewardTierLevel: undefined,
|
||||||
redeemLocation: surprise.redeemLocation,
|
redeemLocation: surprise.redeemLocation,
|
||||||
|
coupons: "coupon" in surprise ? surprise.coupon || [] : [],
|
||||||
categories:
|
categories:
|
||||||
"categories" in surprise ? surprise.categories || [] : [],
|
"categories" in surprise ? surprise.categories || [] : [],
|
||||||
coupons: "coupon" in surprise ? surprise.coupon || [] : [],
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.flatMap((surprises) => (surprises ? [surprises] : []))
|
.flatMap((surprises) => (surprises ? [surprises] : []))
|
||||||
|
|
||||||
return surprises
|
return surprises
|
||||||
|
},
|
||||||
|
"1h"
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
unwrap: protectedProcedure
|
unwrap: protectedProcedure
|
||||||
.input(rewardsUpdateInput)
|
.input(rewardsUpdateInput)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { metrics } from "@opentelemetry/api"
|
import { metrics } from "@opentelemetry/api"
|
||||||
import { unstable_cache } from "next/cache"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import { notFound } from "@/server/errors/trpc"
|
import { notFound } from "@/server/errors/trpc"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
import { generateLoyaltyConfigTag, generateTag } from "@/utils/generateTag"
|
import { generateLoyaltyConfigTag, generateTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -85,8 +85,6 @@ export const getAllCMSRewardRefsSuccessCounter = meter.createCounter(
|
|||||||
"trpc.contentstack.reward.all-success"
|
"trpc.contentstack.reward.all-success"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ONE_HOUR = 60 * 60
|
|
||||||
|
|
||||||
export function getUniqueRewardIds(rewardIds: string[]) {
|
export function getUniqueRewardIds(rewardIds: string[]) {
|
||||||
const uniqueRewardIds = new Set(rewardIds)
|
const uniqueRewardIds = new Set(rewardIds)
|
||||||
return Array.from(uniqueRewardIds)
|
return Array.from(uniqueRewardIds)
|
||||||
@@ -96,8 +94,12 @@ export function getUniqueRewardIds(rewardIds: string[]) {
|
|||||||
* Uses the legacy profile/v1/Profile/tierRewards endpoint.
|
* Uses the legacy profile/v1/Profile/tierRewards endpoint.
|
||||||
* TODO: Delete when the new endpoint is out in production.
|
* TODO: Delete when the new endpoint is out in production.
|
||||||
*/
|
*/
|
||||||
export const getAllCachedApiRewards = unstable_cache(
|
export async function getAllCachedApiRewards(token: string) {
|
||||||
async function (token) {
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
"getAllApiRewards",
|
||||||
|
async () => {
|
||||||
const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, {
|
const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
@@ -129,7 +131,8 @@ export const getAllCachedApiRewards = unstable_cache(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await apiResponse.json()
|
const data = await apiResponse.json()
|
||||||
const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data)
|
const validatedApiTierRewards =
|
||||||
|
validateApiTierRewardsSchema.safeParse(data)
|
||||||
|
|
||||||
if (!validatedApiTierRewards.success) {
|
if (!validatedApiTierRewards.success) {
|
||||||
getAllRewardFailCounter.add(1, {
|
getAllRewardFailCounter.add(1, {
|
||||||
@@ -148,15 +151,19 @@ export const getAllCachedApiRewards = unstable_cache(
|
|||||||
|
|
||||||
return validatedApiTierRewards.data
|
return validatedApiTierRewards.data
|
||||||
},
|
},
|
||||||
["getAllApiRewards"],
|
"1h"
|
||||||
{ revalidate: ONE_HOUR }
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cached for 1 hour.
|
* Cached for 1 hour.
|
||||||
*/
|
*/
|
||||||
export const getCachedAllTierRewards = unstable_cache(
|
export async function getCachedAllTierRewards(token: string) {
|
||||||
async function (token) {
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
"getAllTierRewards",
|
||||||
|
async () => {
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Profile.Reward.allTiers,
|
api.endpoints.v1.Profile.Reward.allTiers,
|
||||||
{
|
{
|
||||||
@@ -191,7 +198,8 @@ export const getCachedAllTierRewards = unstable_cache(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await apiResponse.json()
|
const data = await apiResponse.json()
|
||||||
const validatedApiAllTierRewards = validateApiAllTiersSchema.safeParse(data)
|
const validatedApiAllTierRewards =
|
||||||
|
validateApiAllTiersSchema.safeParse(data)
|
||||||
|
|
||||||
if (!validatedApiAllTierRewards.success) {
|
if (!validatedApiAllTierRewards.success) {
|
||||||
getAllRewardFailCounter.add(1, {
|
getAllRewardFailCounter.add(1, {
|
||||||
@@ -210,9 +218,9 @@ export const getCachedAllTierRewards = unstable_cache(
|
|||||||
|
|
||||||
return validatedApiAllTierRewards.data
|
return validatedApiAllTierRewards.data
|
||||||
},
|
},
|
||||||
["getApiAllTierRewards"],
|
"1h"
|
||||||
{ revalidate: ONE_HOUR }
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
||||||
const tags = rewardIds.map((id) =>
|
const tags = rewardIds.map((id) =>
|
||||||
@@ -235,10 +243,8 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
|||||||
rewardIds,
|
rewardIds,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: rewardIds.map((rewardId) => generateTag(lang, rewardId)),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: rewardIds.map((rewardId) => generateTag(lang, rewardId)),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!refsResponse.data) {
|
if (!refsResponse.data) {
|
||||||
@@ -292,7 +298,10 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
|||||||
locale: lang,
|
locale: lang,
|
||||||
rewardIds,
|
rewardIds,
|
||||||
},
|
},
|
||||||
{ next: { tags }, cache: "force-cache" }
|
{
|
||||||
|
key: tags,
|
||||||
|
ttl: "max",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
cmsRewardsResponse = await request<CmsRewardsResponse>(
|
cmsRewardsResponse = await request<CmsRewardsResponse>(
|
||||||
@@ -301,7 +310,7 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
|||||||
locale: lang,
|
locale: lang,
|
||||||
rewardIds,
|
rewardIds,
|
||||||
},
|
},
|
||||||
{ next: { tags }, cache: "force-cache" }
|
{ key: tags, ttl: "max" }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,8 @@ export const startPageQueryRouter = router({
|
|||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: generateTag(lang, uid),
|
||||||
next: {
|
ttl: "max",
|
||||||
tags: [generateTag(lang, uid)],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!refsResponse.data) {
|
if (!refsResponse.data) {
|
||||||
@@ -118,10 +116,8 @@ export const startPageQueryRouter = router({
|
|||||||
uid,
|
uid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cache: "force-cache",
|
key: tags,
|
||||||
next: {
|
ttl: "max",
|
||||||
tags,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export const countriesSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type Cities = z.infer<typeof citiesSchema>
|
||||||
export const citiesSchema = z
|
export const citiesSchema = z
|
||||||
.object({
|
.object({
|
||||||
data: z.array(citySchema),
|
data: z.array(citySchema),
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { unstable_cache } from "next/cache"
|
|
||||||
|
|
||||||
import { ApiLang } from "@/constants/languages"
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
@@ -16,6 +13,7 @@ import {
|
|||||||
import { toApiLang } from "@/server/utils"
|
import { toApiLang } from "@/server/utils"
|
||||||
|
|
||||||
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
import { cache } from "@/utils/cache"
|
import { cache } from "@/utils/cache"
|
||||||
|
|
||||||
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
||||||
@@ -67,7 +65,6 @@ import type { BedTypeSelection } from "@/types/components/hotelReservation/enter
|
|||||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
|
||||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||||
import type {
|
import type {
|
||||||
HotelsAvailabilityInputSchema,
|
HotelsAvailabilityInputSchema,
|
||||||
@@ -78,8 +75,7 @@ import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
|||||||
|
|
||||||
export const getHotel = cache(
|
export const getHotel = cache(
|
||||||
async (input: HotelInput, serviceToken: string) => {
|
async (input: HotelInput, serviceToken: string) => {
|
||||||
const callable = unstable_cache(
|
const callable = async function (
|
||||||
async function (
|
|
||||||
hotelId: HotelInput["hotelId"],
|
hotelId: HotelInput["hotelId"],
|
||||||
language: HotelInput["language"],
|
language: HotelInput["language"],
|
||||||
isCardOnlyPayment?: HotelInput["isCardOnlyPayment"]
|
isCardOnlyPayment?: HotelInput["isCardOnlyPayment"]
|
||||||
@@ -113,13 +109,6 @@ export const getHotel = cache(
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${serviceToken}`,
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
},
|
},
|
||||||
// needs to clear default option as only
|
|
||||||
// cache or next.revalidate is permitted
|
|
||||||
cache: undefined,
|
|
||||||
next: {
|
|
||||||
revalidate: env.CACHE_TIME_HOTELS,
|
|
||||||
tags: [`${language}:hotel:${hotelId}`],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
@@ -198,17 +187,16 @@ export const getHotel = cache(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return hotelData
|
return hotelData
|
||||||
},
|
|
||||||
[`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`],
|
|
||||||
{
|
|
||||||
revalidate: env.CACHE_TIME_HOTELS,
|
|
||||||
tags: [
|
|
||||||
`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`,
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`,
|
||||||
|
async () => {
|
||||||
return callable(input.hotelId, input.language, input.isCardOnlyPayment)
|
return callable(input.hotelId, input.language, input.isCardOnlyPayment)
|
||||||
|
},
|
||||||
|
"1d"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -226,7 +214,10 @@ export const getHotelsAvailabilityByCity = async (
|
|||||||
bookingCode,
|
bookingCode,
|
||||||
redemption,
|
redemption,
|
||||||
} = input
|
} = input
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
`${cityId}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
|
||||||
|
async () => {
|
||||||
const params: Record<string, string | number> = {
|
const params: Record<string, string | number> = {
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
@@ -252,13 +243,9 @@ export const getHotelsAvailabilityByCity = async (
|
|||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Availability.city(cityId),
|
api.endpoints.v1.Availability.city(cityId),
|
||||||
{
|
{
|
||||||
cache: undefined,
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
next: {
|
|
||||||
revalidate: env.CACHE_TIME_CITY_SEARCH,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
@@ -271,7 +258,6 @@ export const getHotelsAvailabilityByCity = async (
|
|||||||
adults,
|
adults,
|
||||||
children,
|
children,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
redemption,
|
|
||||||
error_type: "http_error",
|
error_type: "http_error",
|
||||||
error: JSON.stringify({
|
error: JSON.stringify({
|
||||||
status: apiResponse.status,
|
status: apiResponse.status,
|
||||||
@@ -290,10 +276,13 @@ export const getHotelsAvailabilityByCity = async (
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return null
|
|
||||||
|
throw new Error("Failed to fetch hotels availability by city")
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
|
const validateAvailabilityData =
|
||||||
|
hotelsAvailabilitySchema.safeParse(apiJson)
|
||||||
if (!validateAvailabilityData.success) {
|
if (!validateAvailabilityData.success) {
|
||||||
metrics.hotelsAvailability.fail.add(1, {
|
metrics.hotelsAvailability.fail.add(1, {
|
||||||
cityId,
|
cityId,
|
||||||
@@ -335,6 +324,9 @@ export const getHotelsAvailabilityByCity = async (
|
|||||||
(hotels) => hotels.attributes
|
(hotels) => hotels.attributes
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"1h"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getHotelsAvailabilityByHotelIds = async (
|
export const getHotelsAvailabilityByHotelIds = async (
|
||||||
@@ -351,12 +343,6 @@ export const getHotelsAvailabilityByHotelIds = async (
|
|||||||
bookingCode,
|
bookingCode,
|
||||||
} = input
|
} = input
|
||||||
|
|
||||||
/**
|
|
||||||
* Since API expects the params appended and not just
|
|
||||||
* a comma separated string we need to initialize the
|
|
||||||
* SearchParams with a sequence of pairs
|
|
||||||
* (hotelIds=810&hotelIds=879&hotelIds=222 etc.)
|
|
||||||
**/
|
|
||||||
const params = new URLSearchParams([
|
const params = new URLSearchParams([
|
||||||
["roomStayStartDate", roomStayStartDate],
|
["roomStayStartDate", roomStayStartDate],
|
||||||
["roomStayEndDate", roomStayEndDate],
|
["roomStayEndDate", roomStayEndDate],
|
||||||
@@ -365,7 +351,21 @@ export const getHotelsAvailabilityByHotelIds = async (
|
|||||||
["bookingCode", bookingCode],
|
["bookingCode", bookingCode],
|
||||||
["language", apiLang],
|
["language", apiLang],
|
||||||
])
|
])
|
||||||
hotelIds.forEach((hotelId) => params.append("hotelIds", hotelId.toString()))
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return cacheClient.cacheOrGet(
|
||||||
|
`${apiLang}:hotels:availability:${hotelIds.join(",")}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
|
||||||
|
async () => {
|
||||||
|
/**
|
||||||
|
* Since API expects the params appended and not just
|
||||||
|
* a comma separated string we need to initialize the
|
||||||
|
* SearchParams with a sequence of pairs
|
||||||
|
* (hotelIds=810&hotelIds=879&hotelIds=222 etc.)
|
||||||
|
**/
|
||||||
|
|
||||||
|
hotelIds.forEach((hotelId) =>
|
||||||
|
params.append("hotelIds", hotelId.toString())
|
||||||
|
)
|
||||||
metrics.hotelsByHotelIdAvailability.counter.add(1, {
|
metrics.hotelsByHotelIdAvailability.counter.add(1, {
|
||||||
hotelIds,
|
hotelIds,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
@@ -381,13 +381,9 @@ export const getHotelsAvailabilityByHotelIds = async (
|
|||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Availability.hotels(),
|
api.endpoints.v1.Availability.hotels(),
|
||||||
{
|
{
|
||||||
cache: undefined,
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${serviceToken}`,
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
},
|
},
|
||||||
next: {
|
|
||||||
revalidate: env.CACHE_TIME_CITY_SEARCH,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
@@ -418,10 +414,12 @@ export const getHotelsAvailabilityByHotelIds = async (
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return null
|
|
||||||
|
throw new Error("Failed to fetch hotels availability by hotelIds")
|
||||||
}
|
}
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
|
const validateAvailabilityData =
|
||||||
|
hotelsAvailabilitySchema.safeParse(apiJson)
|
||||||
if (!validateAvailabilityData.success) {
|
if (!validateAvailabilityData.success) {
|
||||||
metrics.hotelsByHotelIdAvailability.fail.add(1, {
|
metrics.hotelsByHotelIdAvailability.fail.add(1, {
|
||||||
hotelIds,
|
hotelIds,
|
||||||
@@ -461,6 +459,9 @@ export const getHotelsAvailabilityByHotelIds = async (
|
|||||||
(hotels) => hotels.attributes
|
(hotels) => hotels.attributes
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
env.CACHE_TIME_CITY_SEARCH
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hotelQueryRouter = router({
|
export const hotelQueryRouter = router({
|
||||||
@@ -654,7 +655,8 @@ export const hotelQueryRouter = router({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return null
|
|
||||||
|
throw new Error("Failed to fetch selected room availability")
|
||||||
}
|
}
|
||||||
const apiJsonAvailability = await apiResponseAvailability.json()
|
const apiJsonAvailability = await apiResponseAvailability.json()
|
||||||
const validateAvailabilityData =
|
const validateAvailabilityData =
|
||||||
@@ -913,28 +915,12 @@ export const hotelQueryRouter = router({
|
|||||||
const { lang, serviceToken } = ctx
|
const { lang, serviceToken } = ctx
|
||||||
const { country } = input
|
const { country } = input
|
||||||
|
|
||||||
const options: RequestOptionsWithOutBody = {
|
const hotelIds = await getHotelIdsByCountry({
|
||||||
// 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 hotelIdsParams = new URLSearchParams({
|
|
||||||
language: ApiLang.En,
|
|
||||||
country,
|
country,
|
||||||
|
serviceToken: ctx.serviceToken,
|
||||||
})
|
})
|
||||||
const hotelIds = await getHotelIdsByCountry(
|
|
||||||
country,
|
|
||||||
options,
|
|
||||||
hotelIdsParams
|
|
||||||
)
|
|
||||||
|
|
||||||
return await getHotelsByHotelIds(hotelIds, lang, serviceToken)
|
return await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
byCityIdentifier: router({
|
byCityIdentifier: router({
|
||||||
@@ -949,7 +935,7 @@ export const hotelQueryRouter = router({
|
|||||||
serviceToken
|
serviceToken
|
||||||
)
|
)
|
||||||
|
|
||||||
return await getHotelsByHotelIds(hotelIds, lang, serviceToken)
|
return await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
byCSFilter: router({
|
byCSFilter: router({
|
||||||
@@ -959,19 +945,6 @@ export const hotelQueryRouter = router({
|
|||||||
const { locationFilter, hotelsToInclude } = input
|
const { locationFilter, hotelsToInclude } = input
|
||||||
|
|
||||||
const language = ctx.lang
|
const language = ctx.lang
|
||||||
const apiLang = toApiLang(language)
|
|
||||||
const options: RequestOptionsWithOutBody = {
|
|
||||||
// needs to clear default option as only
|
|
||||||
// cache or next.revalidate is permitted
|
|
||||||
cache: undefined,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
||||||
},
|
|
||||||
next: {
|
|
||||||
revalidate: env.CACHE_TIME_HOTELS,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
let hotelsToFetch: string[] = []
|
let hotelsToFetch: string[] = []
|
||||||
|
|
||||||
metrics.hotels.counter.add(1, {
|
metrics.hotels.counter.add(1, {
|
||||||
@@ -991,15 +964,11 @@ export const hotelQueryRouter = router({
|
|||||||
if (hotelsToInclude.length) {
|
if (hotelsToInclude.length) {
|
||||||
hotelsToFetch = hotelsToInclude
|
hotelsToFetch = hotelsToInclude
|
||||||
} else if (locationFilter?.city) {
|
} else if (locationFilter?.city) {
|
||||||
const locationsParams = new URLSearchParams({
|
const locations = await getLocations({
|
||||||
language: apiLang,
|
lang: language,
|
||||||
|
serviceToken: ctx.serviceToken,
|
||||||
|
citiesByCountry: null,
|
||||||
})
|
})
|
||||||
const locations = await getLocations(
|
|
||||||
language,
|
|
||||||
options,
|
|
||||||
locationsParams,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
if (!locations || "error" in locations) {
|
if (!locations || "error" in locations) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -1028,15 +997,11 @@ export const hotelQueryRouter = router({
|
|||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const hotelIdsParams = new URLSearchParams({
|
|
||||||
language: apiLang,
|
const hotelIds = await getHotelIdsByCityId({
|
||||||
city: cityId,
|
|
||||||
})
|
|
||||||
const hotelIds = await getHotelIdsByCityId(
|
|
||||||
cityId,
|
cityId,
|
||||||
options,
|
serviceToken: ctx.serviceToken,
|
||||||
hotelIdsParams
|
})
|
||||||
)
|
|
||||||
|
|
||||||
if (!hotelIds?.length) {
|
if (!hotelIds?.length) {
|
||||||
metrics.hotels.fail.add(1, {
|
metrics.hotels.fail.add(1, {
|
||||||
@@ -1062,15 +1027,10 @@ export const hotelQueryRouter = router({
|
|||||||
|
|
||||||
hotelsToFetch = filteredHotelIds
|
hotelsToFetch = filteredHotelIds
|
||||||
} else if (locationFilter?.country) {
|
} else if (locationFilter?.country) {
|
||||||
const hotelIdsParams = new URLSearchParams({
|
const hotelIds = await getHotelIdsByCountry({
|
||||||
language: ApiLang.En,
|
|
||||||
country: locationFilter.country,
|
country: locationFilter.country,
|
||||||
|
serviceToken: ctx.serviceToken,
|
||||||
})
|
})
|
||||||
const hotelIds = await getHotelIdsByCountry(
|
|
||||||
locationFilter.country,
|
|
||||||
options,
|
|
||||||
hotelIdsParams
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!hotelIds?.length) {
|
if (!hotelIds?.length) {
|
||||||
metrics.hotels.fail.add(1, {
|
metrics.hotels.fail.add(1, {
|
||||||
@@ -1154,43 +1114,29 @@ export const hotelQueryRouter = router({
|
|||||||
}),
|
}),
|
||||||
getAllHotels: router({
|
getAllHotels: router({
|
||||||
get: serviceProcedure.query(async function ({ ctx }) {
|
get: serviceProcedure.query(async function ({ ctx }) {
|
||||||
const apiLang = toApiLang(ctx.lang)
|
const countries = await getCountries({
|
||||||
const params = new URLSearchParams({
|
lang: ctx.lang,
|
||||||
language: apiLang,
|
serviceToken: ctx.serviceToken,
|
||||||
})
|
})
|
||||||
const options: RequestOptionsWithOutBody = {
|
|
||||||
// needs to clear default option as only
|
|
||||||
// cache or next.revalidate is permitted
|
|
||||||
cache: undefined,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
||||||
},
|
|
||||||
next: {
|
|
||||||
revalidate: env.CACHE_TIME_HOTELS,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const countries = await getCountries(options, params, ctx.lang)
|
|
||||||
if (!countries) {
|
if (!countries) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const countryNames = countries.data.map((country) => country.name)
|
const countryNames = countries.data.map((country) => country.name)
|
||||||
const hotelData: HotelDataWithUrl[] = (
|
const hotelData: HotelDataWithUrl[] = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
countryNames.map(async (country) => {
|
countryNames.map(async (country) => {
|
||||||
const countryParams = new URLSearchParams({
|
const hotelIds = await getHotelIdsByCountry({
|
||||||
country: country,
|
|
||||||
})
|
|
||||||
const hotelIds = await getHotelIdsByCountry(
|
|
||||||
country,
|
country,
|
||||||
options,
|
serviceToken: ctx.serviceToken,
|
||||||
countryParams
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const hotels = await getHotelsByHotelIds(
|
const hotels = await getHotelsByHotelIds({
|
||||||
hotelIds,
|
hotelIds,
|
||||||
ctx.lang,
|
lang: ctx.lang,
|
||||||
ctx.serviceToken
|
serviceToken: ctx.serviceToken,
|
||||||
)
|
})
|
||||||
return hotels
|
return hotels
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -1280,50 +1226,43 @@ export const hotelQueryRouter = router({
|
|||||||
}),
|
}),
|
||||||
locations: router({
|
locations: router({
|
||||||
get: serviceProcedure.input(getLocationsInput).query(async function ({
|
get: serviceProcedure.input(getLocationsInput).query(async function ({
|
||||||
input,
|
|
||||||
ctx,
|
ctx,
|
||||||
|
input,
|
||||||
}) {
|
}) {
|
||||||
const lang = input.lang ?? ctx.lang
|
const lang = input.lang ?? ctx.lang
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
`${ctx.lang}:getLocations`,
|
||||||
|
async () => {
|
||||||
|
const countries = await getCountries({
|
||||||
|
lang: lang,
|
||||||
|
serviceToken: ctx.serviceToken,
|
||||||
|
})
|
||||||
|
|
||||||
const searchParams = new URLSearchParams()
|
|
||||||
searchParams.set("language", toApiLang(lang))
|
|
||||||
|
|
||||||
const options: RequestOptionsWithOutBody = {
|
|
||||||
// needs to clear default option as only
|
|
||||||
// cache or next.revalidate is permitted
|
|
||||||
cache: undefined,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
||||||
},
|
|
||||||
next: {
|
|
||||||
revalidate: env.CACHE_TIME_HOTELS,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const countries = await getCountries(options, searchParams, lang)
|
|
||||||
if (!countries) {
|
if (!countries) {
|
||||||
throw new Error("Unable to fetch countries")
|
throw new Error("Unable to fetch countries")
|
||||||
}
|
}
|
||||||
const countryNames = countries.data.map((country) => country.name)
|
const countryNames = countries.data.map((country) => country.name)
|
||||||
const citiesByCountry = await getCitiesByCountry(
|
const citiesByCountry = await getCitiesByCountry({
|
||||||
countryNames,
|
countries: countryNames,
|
||||||
options,
|
serviceToken: ctx.serviceToken,
|
||||||
searchParams,
|
|
||||||
lang
|
|
||||||
)
|
|
||||||
|
|
||||||
const locations = await getLocations(
|
|
||||||
lang,
|
lang,
|
||||||
options,
|
})
|
||||||
searchParams,
|
|
||||||
citiesByCountry
|
const locations = await getLocations({
|
||||||
)
|
lang,
|
||||||
|
serviceToken: ctx.serviceToken,
|
||||||
|
citiesByCountry,
|
||||||
|
})
|
||||||
|
|
||||||
if (!locations || "error" in locations) {
|
if (!locations || "error" in locations) {
|
||||||
throw new Error("Unable to fetch locations")
|
throw new Error("Unable to fetch locations")
|
||||||
}
|
}
|
||||||
|
|
||||||
return locations
|
return locations
|
||||||
|
},
|
||||||
|
"max"
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
map: router({
|
map: router({
|
||||||
@@ -1380,16 +1319,16 @@ export const hotelQueryRouter = router({
|
|||||||
JSON.stringify({ query: { hotelId, params } })
|
JSON.stringify({ query: { hotelId, params } })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return cacheClient.cacheOrGet(
|
||||||
|
`${language}:hotels:meetingRooms:${hotelId}`,
|
||||||
|
async () => {
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Hotel.Hotels.meetingRooms(input.hotelId),
|
api.endpoints.v1.Hotel.Hotels.meetingRooms(input.hotelId),
|
||||||
{
|
{
|
||||||
cache: undefined,
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
},
|
},
|
||||||
next: {
|
|
||||||
revalidate: env.CACHE_TIME_HOTELS,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
@@ -1416,7 +1355,8 @@ export const hotelQueryRouter = router({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return []
|
|
||||||
|
throw new Error("Failed to fetch meeting rooms")
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
@@ -1430,7 +1370,7 @@ export const hotelQueryRouter = router({
|
|||||||
error: validatedMeetingRooms.error,
|
error: validatedMeetingRooms.error,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return []
|
throw badRequestError()
|
||||||
}
|
}
|
||||||
metrics.meetingRooms.success.add(1, {
|
metrics.meetingRooms.success.add(1, {
|
||||||
hotelId,
|
hotelId,
|
||||||
@@ -1441,6 +1381,9 @@ export const hotelQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return validatedMeetingRooms.data.data
|
return validatedMeetingRooms.data.data
|
||||||
|
},
|
||||||
|
env.CACHE_TIME_HOTELS
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
additionalData: safeProtectedServiceProcedure
|
additionalData: safeProtectedServiceProcedure
|
||||||
.input(getAdditionalDataInputSchema)
|
.input(getAdditionalDataInputSchema)
|
||||||
@@ -1458,16 +1401,16 @@ export const hotelQueryRouter = router({
|
|||||||
JSON.stringify({ query: { hotelId, params } })
|
JSON.stringify({ query: { hotelId, params } })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return cacheClient.cacheOrGet(
|
||||||
|
`${language}:hotels:additionalData:${hotelId}`,
|
||||||
|
async () => {
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Hotel.Hotels.additionalData(input.hotelId),
|
api.endpoints.v1.Hotel.Hotels.additionalData(input.hotelId),
|
||||||
{
|
{
|
||||||
cache: undefined,
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
},
|
},
|
||||||
next: {
|
|
||||||
revalidate: env.CACHE_TIME_HOTELS,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
@@ -1494,11 +1437,13 @@ export const hotelQueryRouter = router({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return null
|
|
||||||
|
throw new Error("Unable to fetch additional data for hotel")
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
const validatedAdditionalData = additionalDataSchema.safeParse(apiJson)
|
const validatedAdditionalData =
|
||||||
|
additionalDataSchema.safeParse(apiJson)
|
||||||
|
|
||||||
if (!validatedAdditionalData.success) {
|
if (!validatedAdditionalData.success) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -1519,6 +1464,9 @@ export const hotelQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return validatedAdditionalData.data
|
return validatedAdditionalData.data
|
||||||
|
},
|
||||||
|
env.CACHE_TIME_HOTELS
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
packages: router({
|
packages: router({
|
||||||
get: serviceProcedure
|
get: serviceProcedure
|
||||||
@@ -1628,16 +1576,16 @@ export const hotelQueryRouter = router({
|
|||||||
JSON.stringify({ query: metricsData })
|
JSON.stringify({ query: metricsData })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
const breakfastPackages = await cacheClient.cacheOrGet(
|
||||||
|
`${apiLang}:adults${input.adults}:startDate:${params.StartDate}:endDate:${params.EndDate}`,
|
||||||
|
async () => {
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Package.Breakfast.hotel(input.hotelId),
|
api.endpoints.v1.Package.Breakfast.hotel(input.hotelId),
|
||||||
{
|
{
|
||||||
cache: undefined,
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
},
|
},
|
||||||
next: {
|
|
||||||
revalidate: 60,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
@@ -1664,7 +1612,7 @@ export const hotelQueryRouter = router({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return null
|
throw new Error("Unable to fetch breakfast packages")
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
@@ -1682,7 +1630,8 @@ export const hotelQueryRouter = router({
|
|||||||
error: breakfastPackages.error,
|
error: breakfastPackages.error,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return null
|
|
||||||
|
throw new Error("Unable to parse breakfast packages")
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.breakfastPackage.success.add(1, metricsData)
|
metrics.breakfastPackage.success.add(1, metricsData)
|
||||||
@@ -1693,6 +1642,11 @@ export const hotelQueryRouter = router({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return breakfastPackages.data
|
||||||
|
},
|
||||||
|
"1h"
|
||||||
|
)
|
||||||
|
|
||||||
if (ctx.session?.token) {
|
if (ctx.session?.token) {
|
||||||
const apiUser = await getVerifiedUser({ session: ctx.session })
|
const apiUser = await getVerifiedUser({ session: ctx.session })
|
||||||
if (apiUser && !("error" in apiUser)) {
|
if (apiUser && !("error" in apiUser)) {
|
||||||
@@ -1701,7 +1655,7 @@ export const hotelQueryRouter = router({
|
|||||||
user.membership &&
|
user.membership &&
|
||||||
["L6", "L7"].includes(user.membership.membershipLevel)
|
["L6", "L7"].includes(user.membership.membershipLevel)
|
||||||
) {
|
) {
|
||||||
const freeBreakfastPackage = breakfastPackages.data.find(
|
const freeBreakfastPackage = breakfastPackages.find(
|
||||||
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||||
)
|
)
|
||||||
if (freeBreakfastPackage?.localPrice) {
|
if (freeBreakfastPackage?.localPrice) {
|
||||||
@@ -1711,7 +1665,7 @@ export const hotelQueryRouter = router({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return breakfastPackages.data.filter(
|
return breakfastPackages.filter(
|
||||||
(pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
(pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
@@ -1727,23 +1681,22 @@ export const hotelQueryRouter = router({
|
|||||||
language: apiLang,
|
language: apiLang,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
`${apiLang}:hotel:${input.hotelId}:ancillaries:startDate:${params.StartDate}:endDate:${params.EndDate}`,
|
||||||
|
async () => {
|
||||||
const metricsData = { ...params, hotelId: input.hotelId }
|
const metricsData = { ...params, hotelId: input.hotelId }
|
||||||
metrics.ancillaryPackage.counter.add(1, metricsData)
|
metrics.ancillaryPackage.counter.add(1, metricsData)
|
||||||
console.info(
|
console.info(
|
||||||
"api.package.ancillary start",
|
"api.package.ancillary start",
|
||||||
JSON.stringify({ query: metricsData })
|
JSON.stringify({ query: metricsData })
|
||||||
)
|
)
|
||||||
|
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Package.Ancillary.hotel(input.hotelId),
|
api.endpoints.v1.Package.Ancillary.hotel(input.hotelId),
|
||||||
{
|
{
|
||||||
cache: undefined,
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
},
|
},
|
||||||
next: {
|
|
||||||
revalidate: 60,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
@@ -1799,6 +1752,9 @@ export const hotelQueryRouter = router({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
return ancillaryPackages.data
|
return ancillaryPackages.data
|
||||||
|
},
|
||||||
|
"1h"
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
productTypePriceSchema,
|
|
||||||
productTypePointsSchema,
|
productTypePointsSchema,
|
||||||
|
productTypePriceSchema,
|
||||||
} from "../productTypePrice"
|
} from "../productTypePrice"
|
||||||
|
|
||||||
export const productTypeSchema = z
|
export const productTypeSchema = z
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import deepmerge from "deepmerge"
|
import deepmerge from "deepmerge"
|
||||||
import { unstable_cache } from "next/cache"
|
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
import { toApiLang } from "@/server/utils"
|
import { toApiLang } from "@/server/utils"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
|
|
||||||
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
||||||
import { metrics } from "./metrics"
|
import { metrics } from "./metrics"
|
||||||
import {
|
import {
|
||||||
|
type Cities,
|
||||||
citiesByCountrySchema,
|
citiesByCountrySchema,
|
||||||
citiesSchema,
|
citiesSchema,
|
||||||
countriesSchema,
|
countriesSchema,
|
||||||
@@ -18,12 +20,10 @@ import {
|
|||||||
import { getHotel } from "./query"
|
import { getHotel } from "./query"
|
||||||
|
|
||||||
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
||||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
|
||||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||||
import type {
|
import type {
|
||||||
CitiesGroupedByCountry,
|
CitiesGroupedByCountry,
|
||||||
CityLocation,
|
CityLocation,
|
||||||
HotelLocation,
|
|
||||||
} from "@/types/trpc/routers/hotel/locations"
|
} from "@/types/trpc/routers/hotel/locations"
|
||||||
import type { Endpoint } from "@/lib/api/endpoints"
|
import type { Endpoint } from "@/lib/api/endpoints"
|
||||||
|
|
||||||
@@ -58,18 +58,21 @@ export function getPoiGroupByCategoryName(category: string | undefined) {
|
|||||||
export const locationsAffix = "locations"
|
export const locationsAffix = "locations"
|
||||||
|
|
||||||
export const TWENTYFOUR_HOURS = 60 * 60 * 24
|
export const TWENTYFOUR_HOURS = 60 * 60 * 24
|
||||||
export async function getCity(
|
export async function getCity({
|
||||||
cityUrl: string,
|
cityUrl,
|
||||||
options: RequestOptionsWithOutBody,
|
serviceToken,
|
||||||
lang: Lang,
|
}: {
|
||||||
relationshipCity: HotelLocation["relationships"]["city"]
|
cityUrl: string
|
||||||
) {
|
serviceToken: string
|
||||||
return unstable_cache(
|
}): Promise<Cities> {
|
||||||
async function (locationCityUrl: string) {
|
const cacheClient = await getCacheClient()
|
||||||
const url = new URL(locationCityUrl)
|
return await cacheClient.cacheOrGet(
|
||||||
|
cityUrl,
|
||||||
|
async () => {
|
||||||
|
const url = new URL(cityUrl)
|
||||||
const cityResponse = await api.get(
|
const cityResponse = await api.get(
|
||||||
url.pathname as Endpoint,
|
url.pathname as Endpoint,
|
||||||
options,
|
{ headers: { Authorization: `Bearer ${serviceToken}` } },
|
||||||
url.searchParams
|
url.searchParams
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -81,33 +84,44 @@ export async function getCity(
|
|||||||
const city = citiesSchema.safeParse(cityJson)
|
const city = citiesSchema.safeParse(cityJson)
|
||||||
if (!city.success) {
|
if (!city.success) {
|
||||||
console.info(`Validation of city failed`)
|
console.info(`Validation of city failed`)
|
||||||
console.info(`cityUrl: ${locationCityUrl}`)
|
console.info(`cityUrl: ${cityUrl}`)
|
||||||
console.error(city.error)
|
console.error(city.error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return city.data
|
return city.data
|
||||||
},
|
},
|
||||||
[cityUrl, `${lang}:${relationshipCity}`],
|
"1d"
|
||||||
{ revalidate: TWENTYFOUR_HOURS }
|
)
|
||||||
)(cityUrl)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCountries(
|
export async function getCountries({
|
||||||
options: RequestOptionsWithOutBody,
|
lang,
|
||||||
params: URLSearchParams,
|
serviceToken,
|
||||||
|
}: {
|
||||||
lang: Lang
|
lang: Lang
|
||||||
) {
|
serviceToken: string
|
||||||
return unstable_cache(
|
}) {
|
||||||
async function (searchParams) {
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
`${lang}:${locationsAffix}:countries`,
|
||||||
|
async () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
language: toApiLang(lang),
|
||||||
|
})
|
||||||
|
|
||||||
const countryResponse = await api.get(
|
const countryResponse = await api.get(
|
||||||
api.endpoints.v1.Hotel.countries,
|
api.endpoints.v1.Hotel.countries,
|
||||||
options,
|
{
|
||||||
searchParams
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!countryResponse.ok) {
|
if (!countryResponse.ok) {
|
||||||
return null
|
throw new Error("Unable to fetch countries")
|
||||||
}
|
}
|
||||||
|
|
||||||
const countriesJson = await countryResponse.json()
|
const countriesJson = await countryResponse.json()
|
||||||
@@ -120,114 +134,128 @@ export async function getCountries(
|
|||||||
|
|
||||||
return countries.data
|
return countries.data
|
||||||
},
|
},
|
||||||
[`${lang}:${locationsAffix}:countries`, params.toString()],
|
"1d"
|
||||||
{ revalidate: TWENTYFOUR_HOURS }
|
)
|
||||||
)(params)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCitiesByCountry(
|
export async function getCitiesByCountry({
|
||||||
countries: string[],
|
countries,
|
||||||
options: RequestOptionsWithOutBody,
|
lang,
|
||||||
params: URLSearchParams,
|
onlyPublished = false,
|
||||||
lang: Lang,
|
affix = locationsAffix,
|
||||||
onlyPublished = false, // false by default as it might be used in other places
|
serviceToken,
|
||||||
affix: string = locationsAffix
|
}: {
|
||||||
) {
|
countries: string[]
|
||||||
return unstable_cache(
|
lang: Lang
|
||||||
async function (
|
onlyPublished?: boolean // false by default as it might be used in other places
|
||||||
searchParams: URLSearchParams,
|
affix?: string
|
||||||
searchedCountries: string[]
|
serviceToken: string
|
||||||
) {
|
}): Promise<CitiesGroupedByCountry> {
|
||||||
const citiesGroupedByCountry: CitiesGroupedByCountry = {}
|
const cacheClient = await getCacheClient()
|
||||||
|
const allCitiesByCountries = await Promise.all(
|
||||||
await Promise.all(
|
countries.map(async (country) => {
|
||||||
searchedCountries.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(
|
const countryResponse = await api.get(
|
||||||
api.endpoints.v1.Hotel.Cities.country(country),
|
api.endpoints.v1.Hotel.Cities.country(country),
|
||||||
options,
|
{
|
||||||
searchParams
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!countryResponse.ok) {
|
if (!countryResponse.ok) {
|
||||||
return null
|
throw new Error(`Unable to fetch cities by country ${country}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const countryJson = await countryResponse.json()
|
const countryJson = await countryResponse.json()
|
||||||
const citiesByCountry = citiesByCountrySchema.safeParse(countryJson)
|
const citiesByCountry = citiesByCountrySchema.safeParse(countryJson)
|
||||||
if (!citiesByCountry.success) {
|
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)
|
console.error(citiesByCountry.error)
|
||||||
return null
|
throw new Error(`Unable to parse cities by country ${country}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cities = onlyPublished
|
return { ...citiesByCountry.data, country }
|
||||||
? citiesByCountry.data.data.filter((city) => city.isPublished)
|
},
|
||||||
: citiesByCountry.data.data
|
"1d"
|
||||||
citiesGroupedByCountry[country] = cities
|
)
|
||||||
return true
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return citiesGroupedByCountry
|
const filteredCitiesByCountries = allCitiesByCountries.map((country) => ({
|
||||||
},
|
...country,
|
||||||
[
|
data: onlyPublished
|
||||||
`${lang}:${affix}:cities-by-country`,
|
? country.data.filter((city) => city.isPublished)
|
||||||
params.toString(),
|
: country.data,
|
||||||
JSON.stringify(countries),
|
}))
|
||||||
],
|
|
||||||
{ revalidate: TWENTYFOUR_HOURS }
|
const groupedCitiesByCountry: CitiesGroupedByCountry =
|
||||||
)(params, countries)
|
filteredCitiesByCountries.reduce((acc, { country, data }) => {
|
||||||
|
acc[country] = data
|
||||||
|
return acc
|
||||||
|
}, {} as CitiesGroupedByCountry)
|
||||||
|
|
||||||
|
return groupedCitiesByCountry
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLocations(
|
export async function getLocations({
|
||||||
lang: Lang,
|
lang,
|
||||||
options: RequestOptionsWithOutBody,
|
citiesByCountry,
|
||||||
params: URLSearchParams,
|
serviceToken,
|
||||||
|
}: {
|
||||||
|
lang: Lang
|
||||||
citiesByCountry: CitiesGroupedByCountry | null
|
citiesByCountry: CitiesGroupedByCountry | null
|
||||||
) {
|
serviceToken: string
|
||||||
return unstable_cache(
|
}) {
|
||||||
async function (
|
const cacheClient = await getCacheClient()
|
||||||
searchParams: URLSearchParams,
|
|
||||||
groupedCitiesByCountry: CitiesGroupedByCountry | null
|
return await cacheClient.cacheOrGet(
|
||||||
) {
|
`${lang}:locations`.toLowerCase(),
|
||||||
|
async () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
language: toApiLang(lang),
|
||||||
|
})
|
||||||
|
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Hotel.locations,
|
api.endpoints.v1.Hotel.locations,
|
||||||
options,
|
{
|
||||||
searchParams
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
if (apiResponse.status === 401) {
|
if (apiResponse.status === 401) {
|
||||||
return { error: true, cause: "unauthorized" } as const
|
throw new Error("unauthorized")
|
||||||
} else if (apiResponse.status === 403) {
|
} 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 apiJson = await apiResponse.json()
|
||||||
const verifiedLocations = locationsSchema.safeParse(apiJson)
|
const verifiedLocations = locationsSchema.safeParse(apiJson)
|
||||||
if (!verifiedLocations.success) {
|
if (!verifiedLocations.success) {
|
||||||
console.info(`Locations Verification Failed`)
|
console.info(`Locations Verification Failed`)
|
||||||
console.error(verifiedLocations.error)
|
console.error(verifiedLocations.error)
|
||||||
return null
|
throw new Error("Unable to parse locations")
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
verifiedLocations.data.data.map(async (location) => {
|
verifiedLocations.data.data.map(async (location) => {
|
||||||
if (location.type === "cities") {
|
if (location.type === "cities") {
|
||||||
if (groupedCitiesByCountry) {
|
if (citiesByCountry) {
|
||||||
const country = Object.keys(groupedCitiesByCountry).find(
|
const country = Object.keys(citiesByCountry).find((country) =>
|
||||||
(country) => {
|
citiesByCountry[country].find(
|
||||||
if (
|
|
||||||
groupedCitiesByCountry[country].find(
|
|
||||||
(loc) => loc.name === location.name
|
(loc) => loc.name === location.name
|
||||||
)
|
)
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
if (country) {
|
if (country) {
|
||||||
return {
|
return {
|
||||||
@@ -243,12 +271,10 @@ export async function getLocations(
|
|||||||
}
|
}
|
||||||
} else if (location.type === "hotels") {
|
} else if (location.type === "hotels") {
|
||||||
if (location.relationships.city?.url) {
|
if (location.relationships.city?.url) {
|
||||||
const city = await getCity(
|
const city = await getCity({
|
||||||
location.relationships.city.url,
|
cityUrl: location.relationships.city.url,
|
||||||
options,
|
serviceToken,
|
||||||
lang,
|
})
|
||||||
location.relationships.city
|
|
||||||
)
|
|
||||||
if (city) {
|
if (city) {
|
||||||
return deepmerge(location, {
|
return deepmerge(location, {
|
||||||
relationships: {
|
relationships: {
|
||||||
@@ -263,44 +289,51 @@ export async function getLocations(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[
|
"1d"
|
||||||
`${lang}:${locationsAffix}`,
|
)
|
||||||
params.toString(),
|
|
||||||
JSON.stringify(citiesByCountry),
|
|
||||||
],
|
|
||||||
{ revalidate: TWENTYFOUR_HOURS }
|
|
||||||
)(params, citiesByCountry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHotelIdsByCityId(
|
export async function getHotelIdsByCityId({
|
||||||
cityId: string,
|
cityId,
|
||||||
options: RequestOptionsWithOutBody,
|
serviceToken,
|
||||||
params: URLSearchParams
|
}: {
|
||||||
) {
|
cityId: string
|
||||||
return unstable_cache(
|
serviceToken: string
|
||||||
async function (params: URLSearchParams) {
|
}) {
|
||||||
metrics.hotelIds.counter.add(1, { params: params.toString() })
|
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(
|
console.info(
|
||||||
"api.hotel.hotel-ids start",
|
"api.hotel.hotel-ids start",
|
||||||
JSON.stringify({ params: params.toString() })
|
JSON.stringify({ params: searchParams.toString() })
|
||||||
)
|
)
|
||||||
|
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Hotel.hotels,
|
api.endpoints.v1.Hotel.hotels,
|
||||||
options,
|
{
|
||||||
params
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
searchParams
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
const responseMessage = await apiResponse.text()
|
const responseMessage = await apiResponse.text()
|
||||||
metrics.hotelIds.fail.add(1, {
|
metrics.hotelIds.fail.add(1, {
|
||||||
params: params.toString(),
|
params: searchParams.toString(),
|
||||||
error_type: "http_error",
|
error_type: "http_error",
|
||||||
error: responseMessage,
|
error: responseMessage,
|
||||||
})
|
})
|
||||||
console.error(
|
console.error(
|
||||||
"api.hotel.hotel-ids fetch error",
|
"api.hotel.hotel-ids fetch error",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
params: params.toString(),
|
params: searchParams.toString(),
|
||||||
error: {
|
error: {
|
||||||
status: apiResponse.status,
|
status: apiResponse.status,
|
||||||
statusText: apiResponse.statusText,
|
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 apiJson = await apiResponse.json()
|
||||||
const validatedHotelIds = getHotelIdsSchema.safeParse(apiJson)
|
const validatedHotelIds = getHotelIdsSchema.safeParse(apiJson)
|
||||||
if (!validatedHotelIds.success) {
|
if (!validatedHotelIds.success) {
|
||||||
metrics.hotelIds.fail.add(1, {
|
metrics.hotelIds.fail.add(1, {
|
||||||
params: params.toString(),
|
params: searchParams.toString(),
|
||||||
error_type: "validation_error",
|
error_type: "validation_error",
|
||||||
error: JSON.stringify(validatedHotelIds.error),
|
error: JSON.stringify(validatedHotelIds.error),
|
||||||
})
|
})
|
||||||
console.error(
|
console.error(
|
||||||
"api.hotel.hotel-ids validation error",
|
"api.hotel.hotel-ids validation error",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
params: params.toString(),
|
params: searchParams.toString(),
|
||||||
error: validatedHotelIds.error,
|
error: validatedHotelIds.error,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return []
|
|
||||||
|
throw new Error("Unable to parse data for hotelIds by cityId")
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.hotelIds.success.add(1, { cityId })
|
metrics.hotelIds.success.add(1, { cityId })
|
||||||
console.info(
|
console.info(
|
||||||
"api.hotel.hotel-ids success",
|
"api.hotel.hotel-ids success",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
params: params.toString(),
|
params: searchParams.toString(),
|
||||||
response: validatedHotelIds.data,
|
response: validatedHotelIds.data,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return validatedHotelIds.data
|
return validatedHotelIds.data
|
||||||
},
|
},
|
||||||
[`hotelsByCityId`, params.toString()],
|
env.CACHE_TIME_HOTELS
|
||||||
{ revalidate: env.CACHE_TIME_HOTELS }
|
)
|
||||||
)(params)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHotelIdsByCountry(
|
export async function getHotelIdsByCountry({
|
||||||
country: string,
|
country,
|
||||||
options: RequestOptionsWithOutBody,
|
serviceToken,
|
||||||
params: URLSearchParams
|
}: {
|
||||||
) {
|
country: string
|
||||||
return unstable_cache(
|
serviceToken: string
|
||||||
async function (params: URLSearchParams) {
|
}) {
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
`${country}:hotelsByCountry`,
|
||||||
|
async () => {
|
||||||
metrics.hotelIds.counter.add(1, { country })
|
metrics.hotelIds.counter.add(1, { country })
|
||||||
console.info(
|
console.info(
|
||||||
"api.hotel.hotel-ids start",
|
"api.hotel.hotel-ids start",
|
||||||
JSON.stringify({ query: { country } })
|
JSON.stringify({ query: { country } })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const hotelIdsParams = new URLSearchParams({
|
||||||
|
country,
|
||||||
|
})
|
||||||
|
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Hotel.hotels,
|
api.endpoints.v1.Hotel.hotels,
|
||||||
options,
|
{
|
||||||
params
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hotelIdsParams
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
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()
|
const apiJson = await apiResponse.json()
|
||||||
@@ -401,7 +448,7 @@ export async function getHotelIdsByCountry(
|
|||||||
error: validatedHotelIds.error,
|
error: validatedHotelIds.error,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return []
|
throw new Error("Unable to parse hotelIds by country")
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.hotelIds.success.add(1, { country })
|
metrics.hotelIds.success.add(1, { country })
|
||||||
@@ -412,62 +459,45 @@ export async function getHotelIdsByCountry(
|
|||||||
|
|
||||||
return validatedHotelIds.data
|
return validatedHotelIds.data
|
||||||
},
|
},
|
||||||
[`hotelsByCountry`, params.toString()],
|
env.CACHE_TIME_HOTELS
|
||||||
{ revalidate: env.CACHE_TIME_HOTELS }
|
)
|
||||||
)(params)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHotelIdsByCityIdentifier(
|
export async function getHotelIdsByCityIdentifier(
|
||||||
cityIdentifier: string,
|
cityIdentifier: string,
|
||||||
serviceToken: string
|
serviceToken: string
|
||||||
) {
|
) {
|
||||||
const apiLang = toApiLang(Lang.en)
|
const city = await getCityByCityIdentifier({
|
||||||
const city = await getCityByCityIdentifier(cityIdentifier, serviceToken)
|
cityIdentifier,
|
||||||
|
lang: Lang.en,
|
||||||
|
serviceToken,
|
||||||
|
})
|
||||||
|
|
||||||
if (!city) {
|
if (!city) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const hotelIdsParams = new URLSearchParams({
|
const hotelIds = await getHotelIdsByCityId({
|
||||||
language: apiLang,
|
cityId: city.id,
|
||||||
city: 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
|
return hotelIds
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCityByCityIdentifier(
|
export async function getCityByCityIdentifier({
|
||||||
cityIdentifier: string,
|
cityIdentifier,
|
||||||
|
lang,
|
||||||
|
serviceToken,
|
||||||
|
}: {
|
||||||
|
cityIdentifier: string
|
||||||
|
lang: Lang
|
||||||
serviceToken: string
|
serviceToken: string
|
||||||
) {
|
}) {
|
||||||
const lang = Lang.en
|
const locations = await getLocations({
|
||||||
const apiLang = toApiLang(lang)
|
lang,
|
||||||
const options: RequestOptionsWithOutBody = {
|
citiesByCountry: null,
|
||||||
// needs to clear default option as only
|
serviceToken,
|
||||||
// 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, options, params, null)
|
|
||||||
if (!locations || "error" in locations) {
|
if (!locations || "error" in locations) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -479,11 +509,15 @@ export async function getCityByCityIdentifier(
|
|||||||
return city ?? null
|
return city ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHotelsByHotelIds(
|
export async function getHotelsByHotelIds({
|
||||||
hotelIds: string[],
|
hotelIds,
|
||||||
lang: Lang,
|
lang,
|
||||||
|
serviceToken,
|
||||||
|
}: {
|
||||||
|
hotelIds: string[]
|
||||||
|
lang: Lang
|
||||||
serviceToken: string
|
serviceToken: string
|
||||||
) {
|
}) {
|
||||||
const hotelPages = await getHotelPageUrls(lang)
|
const hotelPages = await getHotelPageUrls(lang)
|
||||||
const hotels = await Promise.all(
|
const hotels = await Promise.all(
|
||||||
hotelIds.map(async (hotelId) => {
|
hotelIds.map(async (hotelId) => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { publicProcedure, router } from "@/server/trpc"
|
import { publicProcedure, router } from "@/server/trpc"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
|
|
||||||
import { jobylonFeedSchema } from "./output"
|
import { jobylonFeedSchema } from "./output"
|
||||||
import {
|
import {
|
||||||
getJobylonFeedCounter,
|
getJobylonFeedCounter,
|
||||||
@@ -29,11 +31,12 @@ export const jobylonQueryRouter = router({
|
|||||||
JSON.stringify({ query: { url: urlString } })
|
JSON.stringify({ query: { url: urlString } })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
"jobylon:feed",
|
||||||
|
async () => {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
cache: "force-cache",
|
cache: "no-cache",
|
||||||
next: {
|
|
||||||
revalidate: TWENTYFOUR_HOURS,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -55,7 +58,10 @@ export const jobylonQueryRouter = router({
|
|||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return null
|
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch Jobylon feed: ${JSON.stringify(error)}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseJson = await response.json()
|
const responseJson = await response.json()
|
||||||
@@ -68,14 +74,15 @@ export const jobylonQueryRouter = router({
|
|||||||
error: JSON.stringify(validatedResponse.error),
|
error: JSON.stringify(validatedResponse.error),
|
||||||
})
|
})
|
||||||
|
|
||||||
console.error(
|
const errorData = JSON.stringify({
|
||||||
"jobylon.feed error",
|
|
||||||
JSON.stringify({
|
|
||||||
query: { url: urlString },
|
query: { url: urlString },
|
||||||
error: validatedResponse.error,
|
error: validatedResponse.error,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.error("jobylon.feed error", errorData)
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse Jobylon feed: ${JSON.stringify(errorData)}`
|
||||||
)
|
)
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getJobylonFeedSuccessCounter.add(1, {
|
getJobylonFeedSuccessCounter.add(1, {
|
||||||
@@ -89,6 +96,9 @@ export const jobylonQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return validatedResponse.data
|
return validatedResponse.data
|
||||||
|
},
|
||||||
|
"1d"
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -641,11 +641,9 @@ export const userQueryRouter = router({
|
|||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
api.endpoints.v1.Profile.Transaction.friendTransactions,
|
api.endpoints.v1.Profile.Transaction.friendTransactions,
|
||||||
{
|
{
|
||||||
cache: undefined, // override defaultOptions
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||||
},
|
},
|
||||||
next: { revalidate: 30 * 60 * 1000 },
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { metrics } from "@opentelemetry/api"
|
import { metrics, trace } from "@opentelemetry/api"
|
||||||
import { revalidateTag, unstable_cache } from "next/cache"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
import { generateServiceTokenTag } from "@/utils/generateTag"
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
|
|
||||||
import type { ServiceTokenResponse } from "@/types/tokens"
|
import type { ServiceTokenResponse } from "@/types/tokens"
|
||||||
|
|
||||||
@@ -12,13 +11,49 @@ const meter = metrics.getMeter("trpc.context.serviceToken")
|
|||||||
const fetchServiceTokenCounter = meter.createCounter(
|
const fetchServiceTokenCounter = meter.createCounter(
|
||||||
"trpc.context.serviceToken.fetch-new-token"
|
"trpc.context.serviceToken.fetch-new-token"
|
||||||
)
|
)
|
||||||
const fetchTempServiceTokenCounter = meter.createCounter(
|
|
||||||
"trpc.context.serviceToken.fetch-temporary"
|
|
||||||
)
|
|
||||||
const fetchServiceTokenFailCounter = meter.createCounter(
|
const fetchServiceTokenFailCounter = meter.createCounter(
|
||||||
"trpc.context.serviceToken.fetch-fail"
|
"trpc.context.serviceToken.fetch-fail"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export async function getServiceToken() {
|
||||||
|
const tracer = trace.getTracer("getServiceToken")
|
||||||
|
|
||||||
|
return await tracer.startActiveSpan("getServiceToken", async () => {
|
||||||
|
let scopes: string[] = []
|
||||||
|
if (env.ENABLE_BOOKING_FLOW) {
|
||||||
|
scopes = ["profile", "hotel", "booking", "package", "availability"]
|
||||||
|
} else {
|
||||||
|
scopes = ["profile"]
|
||||||
|
}
|
||||||
|
const cacheKey = getServiceTokenCacheKey(scopes)
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
const token =
|
||||||
|
await cacheClient.get<Awaited<ReturnType<typeof getJwt>>>(cacheKey)
|
||||||
|
console.log("[DEBUG] getServiceToken", typeof token, token)
|
||||||
|
if (!token || token.expiresAt < Date.now()) {
|
||||||
|
return await tracer.startActiveSpan("fetch new token", async () => {
|
||||||
|
const newToken = await getJwt(scopes)
|
||||||
|
const relativeTime = (newToken.expiresAt - Date.now()) / 1000
|
||||||
|
await cacheClient.set(cacheKey, newToken, relativeTime)
|
||||||
|
|
||||||
|
return newToken.jwt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.jwt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJwt(scopes: string[]) {
|
||||||
|
fetchServiceTokenCounter.add(1)
|
||||||
|
const jwt = await fetchServiceToken(scopes)
|
||||||
|
|
||||||
|
const expiresAt = Date.now() + jwt.expires_in * 1000
|
||||||
|
return { expiresAt, jwt }
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchServiceToken(scopes: string[]) {
|
async function fetchServiceToken(scopes: string[]) {
|
||||||
fetchServiceTokenCounter.add(1)
|
fetchServiceTokenCounter.add(1)
|
||||||
|
|
||||||
@@ -69,41 +104,6 @@ async function fetchServiceToken(scopes: string[]) {
|
|||||||
return response.json() as Promise<ServiceTokenResponse>
|
return response.json() as Promise<ServiceTokenResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServiceToken() {
|
function getServiceTokenCacheKey(scopes: string[]): string {
|
||||||
let scopes: string[] = []
|
return `serviceToken:${scopes.join(",")}`
|
||||||
if (env.ENABLE_BOOKING_FLOW) {
|
|
||||||
scopes = ["profile", "hotel", "booking", "package", "availability"]
|
|
||||||
} else {
|
|
||||||
scopes = ["profile"]
|
|
||||||
}
|
|
||||||
|
|
||||||
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 cachedJwt.jwt
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,9 @@ export const safeProtectedProcedure = baseProcedure.use(async function (opts) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const serviceProcedure = baseProcedure.use(async (opts) => {
|
export const serviceProcedure = baseProcedure.use(async (opts) => {
|
||||||
const { access_token } = await getServiceToken()
|
const token = await getServiceToken()
|
||||||
|
console.log("[DEBUG] token", typeof token, token)
|
||||||
|
const { access_token } = token
|
||||||
if (!access_token) {
|
if (!access_token) {
|
||||||
throw internalServerError(`[serviceProcedure] No service token`)
|
throw internalServerError(`[serviceProcedure] No service token`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,23 @@
|
|||||||
|
import { getCacheClient } from "@/services/dataCache"
|
||||||
import { resolve as resolveEntry } from "@/utils/entry"
|
import { resolve as resolveEntry } from "@/utils/entry"
|
||||||
|
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
const entryResponseCache: Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
contentType: string | null
|
|
||||||
uid: string | null
|
|
||||||
expiresAt: number
|
|
||||||
}
|
|
||||||
> = new Map()
|
|
||||||
|
|
||||||
let size: number = 0
|
|
||||||
|
|
||||||
export const fetchAndCacheEntry = async (path: string, lang: Lang) => {
|
export const fetchAndCacheEntry = async (path: string, lang: Lang) => {
|
||||||
const cacheKey = `${path + lang}`
|
path = path || "/"
|
||||||
const cachedResponse = entryResponseCache.get(cacheKey)
|
const cacheKey = `${lang}:resolveentry:${path}`
|
||||||
|
const cache = await getCacheClient()
|
||||||
if (cachedResponse && cachedResponse.expiresAt > Date.now() / 1000) {
|
|
||||||
console.log("[CMS MIDDLEWARE]: CACHE HIT")
|
|
||||||
return cachedResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cachedResponse && cachedResponse.expiresAt < Date.now() / 1000) {
|
|
||||||
console.log("[CMS MIDDLEWARE]: CACHE STALE")
|
|
||||||
size -= JSON.stringify(cachedResponse).length
|
|
||||||
entryResponseCache.delete(cacheKey)
|
|
||||||
} else {
|
|
||||||
console.log("[CMS MIDDLEWARE]: CACHE MISS")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return cache.cacheOrGet(
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
const { contentType, uid } = await resolveEntry(path, lang)
|
const { contentType, uid } = await resolveEntry(path, lang)
|
||||||
let expiresAt = Date.now() / 1000
|
|
||||||
if (!contentType || !uid) {
|
|
||||||
expiresAt += 600
|
|
||||||
} else {
|
|
||||||
expiresAt += 3600 * 12
|
|
||||||
}
|
|
||||||
const entryCache = { contentType, uid, expiresAt }
|
|
||||||
size += JSON.stringify(entryCache).length
|
|
||||||
console.log("[CMS MIDDLEWARE] Adding to cache", entryCache)
|
|
||||||
console.log("[CMS MIDDLEWARE] Cache size (total)", size)
|
|
||||||
entryResponseCache.set(cacheKey, entryCache)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contentType,
|
contentType,
|
||||||
uid,
|
uid,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"1d"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user