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
Loading…
Add table
Add a link
Reference in a new issue