feat(backend): Wrap getAPI met Redis
This commit is contained in:
		
							parent
							
								
									207df530b9
								
							
						
					
					
						commit
						13e3926ac0
					
				
					 6 changed files with 173 additions and 5854 deletions
				
			
		|  | @ -38,6 +38,7 @@ | ||||||
|         "loki-logger-ts": "^1.0.2", |         "loki-logger-ts": "^1.0.2", | ||||||
|         "marked": "^15.0.7", |         "marked": "^15.0.7", | ||||||
|         "nanoid": "^5.1.5", |         "nanoid": "^5.1.5", | ||||||
|  |         "redis": "^5.0.1", | ||||||
|         "response-time": "^2.3.3", |         "response-time": "^2.3.3", | ||||||
|         "swagger-ui-express": "^5.0.1", |         "swagger-ui-express": "^5.0.1", | ||||||
|         "uuid": "^11.1.0", |         "uuid": "^11.1.0", | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { DWENGO_API_BASE } from '../config.js'; | import { DWENGO_API_BASE } from '../config.js'; | ||||||
| import { fetchWithLogging } from '../util/api-helper.js'; | import { fetchRemote } from '../util/api-helper.js'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|     FilteredLearningObject, |     FilteredLearningObject, | ||||||
|  | @ -39,7 +39,7 @@ function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLear | ||||||
|  */ |  */ | ||||||
| export async function getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> { | export async function getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> { | ||||||
|     const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; |     const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; | ||||||
|     const metadata = await fetchWithLogging<LearningObjectMetadata>( |     const metadata = await fetchRemote<LearningObjectMetadata>( | ||||||
|         metadataUrl, |         metadataUrl, | ||||||
|         `Metadata for Learning Object HRUID "${hruid}" (language ${language})` |         `Metadata for Learning Object HRUID "${hruid}" (language ${language})` | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { DWENGO_API_BASE } from '../../config.js'; | import { DWENGO_API_BASE } from '../../config.js'; | ||||||
| import { fetchWithLogging } from '../../util/api-helper.js'; | import { fetchRemote } from '../../util/api-helper.js'; | ||||||
| import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; | import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; | ||||||
| import { LearningObjectProvider } from './learning-object-provider.js'; | import { LearningObjectProvider } from './learning-object-provider.js'; | ||||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | import { getLogger, Logger } from '../../logging/initalize.js'; | ||||||
|  | @ -88,7 +88,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> { |     async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> { | ||||||
|         const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; |         const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; | ||||||
|         const metadata = await fetchWithLogging<LearningObjectMetadata>( |         const metadata = await fetchRemote<LearningObjectMetadata>( | ||||||
|             metadataUrl, |             metadataUrl, | ||||||
|             `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, |             `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, | ||||||
|             { |             { | ||||||
|  | @ -124,7 +124,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> { |     async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> { | ||||||
|         const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; |         const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; | ||||||
|         const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { |         const html = await fetchRemote<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { | ||||||
|             params: { ...id }, |             params: { ...id }, | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { fetchWithLogging } from '../../util/api-helper.js'; | import { fetchRemote } from '../../util/api-helper.js'; | ||||||
| import { DWENGO_API_BASE } from '../../config.js'; | import { DWENGO_API_BASE } from '../../config.js'; | ||||||
| import { LearningPathProvider } from './learning-path-provider.js'; | import { LearningPathProvider } from './learning-path-provider.js'; | ||||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | import { getLogger, Logger } from '../../logging/initalize.js'; | ||||||
|  | @ -20,7 +20,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | ||||||
|         const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; |         const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; | ||||||
|         const params = { pathIdList: JSON.stringify({ hruids }), language }; |         const params = { pathIdList: JSON.stringify({ hruids }), language }; | ||||||
| 
 | 
 | ||||||
|         const learningPaths = await fetchWithLogging<LearningPath[]>(apiUrl, `Learning paths for ${source}`, { params }); |         const learningPaths = await fetchRemote<LearningPath[]>(apiUrl, `Learning paths for ${source}`, { params }); | ||||||
| 
 | 
 | ||||||
|         if (!learningPaths || learningPaths.length === 0) { |         if (!learningPaths || learningPaths.length === 0) { | ||||||
|             logger.warn(`⚠️ WARNING: No learning paths found for ${source}.`); |             logger.warn(`⚠️ WARNING: No learning paths found for ${source}.`); | ||||||
|  | @ -42,7 +42,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | ||||||
|         const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; |         const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; | ||||||
|         const params = { all: query, language }; |         const params = { all: query, language }; | ||||||
| 
 | 
 | ||||||
|         const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params }); |         const searchResults = await fetchRemote<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params }); | ||||||
|         return searchResults ?? []; |         return searchResults ?? []; | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,27 +1,75 @@ | ||||||
| import axios, { AxiosRequestConfig } from 'axios'; | import axios, { AxiosRequestConfig } from 'axios'; | ||||||
| import { getLogger, Logger } from '../logging/initalize.js'; | import { getLogger, Logger } from '../logging/initalize.js'; | ||||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
|  | import { getCacheClient } from '../caching.js'; | ||||||
|  | import { envVars, getEnvVar, getNumericEnvVar } from './envVars.js'; | ||||||
| 
 | 
 | ||||||
| const logger: Logger = getLogger(); | const logger: Logger = getLogger(); | ||||||
|  | const runMode: string = getEnvVar(envVars.RunMode); | ||||||
|  | const prefix: string = getEnvVar(envVars.CacheKeyPrefix); | ||||||
|  | 
 | ||||||
|  | interface Options { | ||||||
|  |     params?: Record<string, unknown> | LearningObjectIdentifier; | ||||||
|  |     query?: Record<string, unknown>; | ||||||
|  |     responseType?: 'json' | 'text'; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Utility function to fetch data from an API endpoint with error handling. |  * Utility function to fetch data from an API endpoint with error handling and caching. | ||||||
|  * Logs errors but does NOT throw exceptions to keep the system running. |  * Logs errors but does NOT throw exceptions to keep the system running. | ||||||
|  * |  * | ||||||
|  * @param url The API endpoint to fetch from. |  * @param url The API endpoint to fetch from. | ||||||
|  * @param description A short description of what is being fetched (for logging). |  * @param description A short description of what is being fetched (for logging). | ||||||
|  * @param options Contains further options such as params (the query params) and responseType (whether the response |  * @param options Contains further options such as params (the query params) and responseType (whether the response | ||||||
|  *                should be parsed as JSON ("json") or whether it should be returned as plain text ("text") |  *                should be parsed as JSON ("json") or whether it should be returned as plain text ("text") | ||||||
|  |  * @param cacheTTL Time-to-live for the cache in seconds (default: 60 seconds). | ||||||
|  * @returns The response data if successful, or null if an error occurs. |  * @returns The response data if successful, or null if an error occurs. | ||||||
|  */ |  */ | ||||||
| export async function fetchWithLogging<T>( | export async function fetchRemote<T>( | ||||||
|     url: string, |     url: string, | ||||||
|     description: string, |     description: string, | ||||||
|     options?: { |     options?: Options, | ||||||
|         params?: Record<string, unknown> | LearningObjectIdentifier; |     cacheTTL?: number | ||||||
|         query?: Record<string, unknown>; | ): Promise<T | null> { | ||||||
|         responseType?: 'json' | 'text'; |     if (runMode !== 'dev') { | ||||||
|  |         return fetchWithCache<T>(url, description, options, cacheTTL); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     getLogger().info(`🔄 INFO: Bypassing cache for ${description} at "${url}".`); | ||||||
|  |     return fetchWithLogging(url, description, options); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchWithCache<T>( | ||||||
|  |     url: string, | ||||||
|  |     description: string, | ||||||
|  |     options?: Options, | ||||||
|  |     cacheTTL?: number | ||||||
|  | ): Promise<T | null> { | ||||||
|  |     // Create a unique cache key based on the URL and options
 | ||||||
|  |     const cacheKey = `${prefix}:${url}${options?.params ? JSON.stringify(options.params) : ''}`; | ||||||
|  |     const cacheClient = await getCacheClient(); | ||||||
|  | 
 | ||||||
|  |     const cachedData = await cacheClient.get(cacheKey); | ||||||
|  |     if (cachedData !== null && cachedData !== undefined) { // TODO What should this condition actually be?
 | ||||||
|  |         // Cache hit! :)
 | ||||||
|  |         getLogger().debug(`✅ INFO: Cache hit for ${description} at "${url}".`); | ||||||
|  |         return JSON.parse(cachedData) as T; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Cache miss :(
 | ||||||
|  |     logger.debug(`🔄 INFO: Cache miss for ${description} at "${url}". Fetching data...`); | ||||||
|  |     const response = await fetchWithLogging<T>(url, description, options); | ||||||
|  |     logger.debug(`🔄 INFO: Fetched data for ${description} at "${url}".`); | ||||||
|  |     await cacheClient.setEx(cacheKey, cacheTTL || getNumericEnvVar(envVars.CacheTTL), JSON.stringify(response)); | ||||||
|  |     logger.debug(`✅ INFO: Cached response for ${description} at "${url}" for ${cacheTTL} seconds.`); | ||||||
|  | 
 | ||||||
|  |     return response; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchWithLogging<T>( | ||||||
|  |     url: string, | ||||||
|  |     description: string, | ||||||
|  |     options?: Options, | ||||||
| ): Promise<T | null> { | ): Promise<T | null> { | ||||||
|     try { |     try { | ||||||
|         const config: AxiosRequestConfig = options || {}; |         const config: AxiosRequestConfig = options || {}; | ||||||
|  | @ -34,7 +82,7 @@ export async function fetchWithLogging<T>( | ||||||
|                     logger.debug(`❌ ERROR: ${description} not found (404) at "${url}".`); |                     logger.debug(`❌ ERROR: ${description} not found (404) at "${url}".`); | ||||||
|                 } else { |                 } else { | ||||||
|                     logger.debug( |                     logger.debug( | ||||||
|                         `❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")` |                         `❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")`, | ||||||
|                     ); |                     ); | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|  |  | ||||||
							
								
								
									
										5948
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										5948
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Reference in a new issue