diff --git a/apps/web/config/graphql/ttl.ts b/apps/web/config/graphql/ttl.ts new file mode 100644 index 0000000..34c02a1 --- /dev/null +++ b/apps/web/config/graphql/ttl.ts @@ -0,0 +1,62 @@ +import { seconds } from '@/utils/time'; + +export const queryTTL: Record = { + GetAddProductType: seconds().fromHours(12), + GetAddproductTypes: seconds().fromHours(12), + GetAgent: seconds().fromHours(12), + GetBrand: seconds().fromHours(3), + GetBrands: seconds().fromHours(3), + GetCoefficients: seconds().fromHours(12), + GetConfiguration: seconds().fromHours(3), + GetConfigurations: seconds().fromMinutes(15), + GetCurrencyChanges: seconds().fromHours(1), + GetDealer: seconds().fromHours(1), + GetDealerPerson: seconds().fromHours(1), + GetDealerPersons: seconds().fromHours(1), + GetDealers: seconds().fromMinutes(15), + GetFuelCards: seconds().fromHours(12), + GetGPSBrands: seconds().fromHours(24), + GetGPSModels: seconds().fromHours(24), + GetImportProgram: seconds().fromHours(12), + GetInsNSIBTypes: seconds().fromHours(12), + GetInsuranceCompanies: seconds().fromHours(12), + GetInsuranceCompany: seconds().fromHours(12), + GetLead: false, + GetLeadUrl: seconds().fromHours(12), + GetLeads: false, + GetLeaseObjectType: seconds().fromHours(24), + GetLeaseObjectTypes: seconds().fromHours(24), + GetLeasingWithoutKaskoTypes: seconds().fromHours(12), + GetModel: seconds().fromHours(3), + GetModels: seconds().fromMinutes(15), + GetOpportunities: false, + GetOpportunity: false, + GetOpportunityUrl: seconds().fromHours(12), + GetProduct: seconds().fromHours(12), + GetProducts: seconds().fromHours(12), + GetQuote: false, + GetQuoteData: false, + GetQuoteUrl: seconds().fromHours(12), + GetQuotes: false, + GetRate: seconds().fromHours(12), + GetRates: seconds().fromHours(12), + GetRegion: seconds().fromHours(24), + GetRegions: seconds().fromHours(24), + GetRegistrationTypes: seconds().fromHours(12), + GetRewardCondition: seconds().fromHours(1), + GetRewardConditions: seconds().fromHours(1), + GetRoles: seconds().fromHours(12), + GetSotCoefficientType: seconds().fromHours(12), + GetSubsidies: seconds().fromHours(12), + GetSubsidy: seconds().fromHours(12), + GetSystemUser: seconds().fromHours(12), + GetTarif: seconds().fromHours(12), + GetTarifs: seconds().fromHours(12), + GetTechnicalCards: seconds().fromHours(12), + GetTelematicTypes: seconds().fromHours(12), + GetTown: seconds().fromHours(24), + GetTowns: seconds().fromHours(24), + GetTrackerTypes: seconds().fromHours(12), + GetTransactionCurrencies: seconds().fromHours(12), + GetTransactionCurrency: seconds().fromHours(12), +}; diff --git a/apps/web/config/schema/env.js b/apps/web/config/schema/env.js index 59ccf2a..243a18f 100644 --- a/apps/web/config/schema/env.js +++ b/apps/web/config/schema/env.js @@ -3,6 +3,11 @@ const { z } = require('zod'); const envSchema = z.object({ BASE_PATH: z.string().optional().default(''), PORT: z.string().optional(), + REDIS_HOST: z.string(), + REDIS_PORT: z + .string() + .transform((val) => Number.parseInt(val, 10)) + .default('6379'), SENTRY_AUTH_TOKEN: z.string(), SENTRY_DSN: z.string(), SENTRY_ENVIRONMENT: z.string(), @@ -16,7 +21,6 @@ const envSchema = z.object({ URL_CRM_CREATEKP_DIRECT: z.string(), URL_CRM_DOWNLOADKP_BASE: z.string(), URL_CRM_GRAPHQL_DIRECT: z.string(), - URL_CRM_GRAPHQL_PROXY: z.string().default('http://api:3001/proxy/graphql'), URL_ELT_KASKO_DIRECT: z.string(), URL_ELT_OSAGO_DIRECT: z.string(), URL_GET_USER_DIRECT: z.string(), diff --git a/apps/web/config/urls.ts b/apps/web/config/urls.ts index f06c419..9e43944 100644 --- a/apps/web/config/urls.ts +++ b/apps/web/config/urls.ts @@ -21,7 +21,7 @@ function getUrls() { PORT, URL_ELT_KASKO_DIRECT, URL_ELT_OSAGO_DIRECT, - URL_CRM_GRAPHQL_PROXY, + URL_CRM_GRAPHQL_DIRECT, URL_CACHE_GET_QUERIES_DIRECT, URL_CACHE_DELETE_QUERY_DIRECT, URL_CACHE_RESET_QUERIES_DIRECT, @@ -41,7 +41,7 @@ function getUrls() { URL_CORE_FINGAP: URL_CORE_FINGAP_DIRECT, URL_CRM_CREATEKP: URL_CRM_CREATEKP_DIRECT, URL_CRM_DOWNLOADKP: withBasePath(urls.URL_CRM_DOWNLOADKP_PROXY), - URL_CRM_GRAPHQL: URL_CRM_GRAPHQL_PROXY, + URL_CRM_GRAPHQL: URL_CRM_GRAPHQL_DIRECT, URL_ELT_KASKO: URL_ELT_KASKO_DIRECT, URL_ELT_OSAGO: URL_ELT_OSAGO_DIRECT, URL_GET_USER: URL_GET_USER_DIRECT, diff --git a/apps/web/constants/urls.js b/apps/web/constants/urls.js index e20cd01..59bb0fe 100644 --- a/apps/web/constants/urls.js +++ b/apps/web/constants/urls.js @@ -8,7 +8,7 @@ module.exports = { URL_CORE_FINGAP_PROXY: '/api/core/fingap', URL_CRM_CREATEKP_PROXY: '/api/crm/create-kp', URL_CRM_DOWNLOADKP_PROXY: '/api/crm/download-kp', - URL_CRM_GRAPHQL_PROXY: '/api/graphql/crm', + URL_CRM_GRAPHQL_PROXY: '/api/crm/graphql', URL_ELT_KASKO_PROXY: '/api/elt/kasko', URL_ELT_OSAGO_PROXY: '/api/elt/osago', URL_GET_USER_PROXY: '/api/auth/user', diff --git a/apps/web/next.config.js b/apps/web/next.config.js index ca3cbdc..efb2858 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -49,10 +49,6 @@ module.exports = withSentryConfig( async rewrites() { return [ - { - destination: env.URL_CRM_GRAPHQL_PROXY + '/:path*', - source: urls.URL_CRM_GRAPHQL_PROXY + '/:path*', - }, { destination: env.URL_CRM_DOWNLOADKP_BASE + '/:path*', source: urls.URL_CRM_DOWNLOADKP_PROXY + '/:path*', diff --git a/apps/web/package.json b/apps/web/package.json index ef9f72a..fc92d04 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,6 +26,7 @@ "@trpc/server": "^10.45.1", "axios": "^1.6.7", "dayjs": "^1.11.10", + "ioredis": "^5.3.2", "mobx": "^6.12.0", "mobx-react-lite": "^4.0.5", "modern-normalize": "^2.0.0", diff --git a/apps/web/pages/api/crm/graphql.ts b/apps/web/pages/api/crm/graphql.ts new file mode 100644 index 0000000..d03768b --- /dev/null +++ b/apps/web/pages/api/crm/graphql.ts @@ -0,0 +1,50 @@ +import { queryTTL } from '@/config/graphql/ttl'; +import getUrls from '@/config/urls'; +import { createRedisInstance } from '@/redis/client'; +import { HttpError } from '@/utils/error'; +import type { ApolloQueryResult, GraphQLRequest } from '@apollo/client'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +const { URL_CRM_GRAPHQL } = getUrls(); + +const redis = createRedisInstance(); + +function getHeaders(req: NextApiRequest) { + const headers = new Headers(); + Object.keys(req.headers).forEach((key) => { + const value = req.headers[key]; + if (value !== undefined && typeof value === 'string') headers.set(key, value); + }); + + return headers; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const headers = getHeaders(req); + + const { operationName, variables } = req.body as GraphQLRequest; + const key = `${operationName} ${JSON.stringify(variables)}`; + const cached = await redis.get(key); + + if (cached) return res.send({ ...JSON.parse(cached), cached: true }); + + const response = await fetch(URL_CRM_GRAPHQL, { + body: JSON.stringify(req.body), + headers, + method: req.method, + }); + + const data = (await response.json()) as ApolloQueryResult; + + if (!response.ok || data?.error || data?.errors?.length) { + throw new HttpError(response.statusText, response.status || 500); + } + + if (operationName) { + const ttl = queryTTL[operationName]; + + if (data && ttl !== false) await redis.set(key, JSON.stringify(data), 'EX', ttl); + } + + return res.send(data); +} diff --git a/apps/web/redis/client.ts b/apps/web/redis/client.ts new file mode 100644 index 0000000..e46cdb8 --- /dev/null +++ b/apps/web/redis/client.ts @@ -0,0 +1,36 @@ +/* eslint-disable no-console */ +import envSchema from '@/config/schema/env'; +import type { RedisOptions } from 'ioredis'; +import { Redis } from 'ioredis'; + +const { REDIS_HOST, REDIS_PORT } = envSchema.parse(process.env); + +export function createRedisInstance() { + try { + const options: RedisOptions = { + enableAutoPipelining: true, + host: REDIS_HOST, + lazyConnect: true, + maxRetriesPerRequest: 0, + port: REDIS_PORT, + retryStrategy: (times: number) => { + if (times > 3) { + throw new Error(`[Redis] Could not connect after ${times} attempts`); + } + + return Math.min(times * 200, 1000); + }, + showFriendlyErrorStack: true, + }; + + const redis = new Redis(options); + + redis.on('error', (error: unknown) => { + console.warn('[Redis] Error connecting', error); + }); + + return redis; + } catch (error) { + throw new Error(`[Redis] Could not create a Redis instance. ${error}`); + } +} diff --git a/apps/web/utils/time.ts b/apps/web/utils/time.ts new file mode 100644 index 0000000..e1e34ca --- /dev/null +++ b/apps/web/utils/time.ts @@ -0,0 +1,13 @@ +export function seconds() { + return { + fromDays(days: number) { + return days * 24 * 60 * 60; + }, + fromHours(hours: number) { + return hours * 60 * 60; + }, + fromMinutes(minutes: number) { + return minutes * 60; + }, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d61b84..b50f0b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,6 +156,9 @@ importers: dayjs: specifier: ^1.11.10 version: 1.11.10 + ioredis: + specifier: ^5.3.2 + version: 5.3.2 mobx: specifier: ^6.12.0 version: 6.12.0