MERGE: dev ino feat/service-layer
This commit is contained in:
		
						commit
						6404335040
					
				
					 220 changed files with 12582 additions and 10400 deletions
				
			
		|  | @ -1,6 +1,7 @@ | |||
| import axios, { AxiosRequestConfig } from 'axios'; | ||||
| import { getLogger, Logger } from '../logging/initalize.js'; | ||||
| 
 | ||||
| // !!!! when logger is done -> change
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
| /** | ||||
|  * Utility function to fetch data from an API endpoint with error handling. | ||||
|  | @ -8,35 +9,34 @@ import axios, { AxiosRequestConfig } from 'axios'; | |||
|  * | ||||
|  * @param url The API endpoint to fetch from. | ||||
|  * @param description A short description of what is being fetched (for logging). | ||||
|  * @param params | ||||
|  * @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") | ||||
|  * @returns The response data if successful, or null if an error occurs. | ||||
|  */ | ||||
| export async function fetchWithLogging<T>( | ||||
|     url: string, | ||||
|     description: string, | ||||
|     params?: Record<string, any> | ||||
|     options?: { | ||||
|         params?: Record<string, any>; | ||||
|         query?: Record<string, any>; | ||||
|         responseType?: 'json' | 'text'; | ||||
|     } | ||||
| ): Promise<T | null> { | ||||
|     try { | ||||
|         const config: AxiosRequestConfig = params ? { params } : {}; | ||||
| 
 | ||||
|         const config: AxiosRequestConfig = options || {}; | ||||
|         const response = await axios.get<T>(url, config); | ||||
|         return response.data; | ||||
|     } catch (error: any) { | ||||
|         if (error.response) { | ||||
|             if (error.response.status === 404) { | ||||
|                 console.error( | ||||
|                     `❌ ERROR: ${description} not found (404) at "${url}".` | ||||
|                 ); | ||||
|                 logger.debug(`❌ ERROR: ${description} not found (404) at "${url}".`); | ||||
|             } else { | ||||
|                 console.error( | ||||
|                 logger.debug( | ||||
|                     `❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")` | ||||
|                 ); | ||||
|             } | ||||
|         } else { | ||||
|             console.error( | ||||
|                 `❌ ERROR: Network or unexpected error when fetching ${description}:`, | ||||
|                 error.message | ||||
|             ); | ||||
|             logger.debug(`❌ ERROR: Network or unexpected error when fetching ${description}:`, error.message); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  |  | |||
							
								
								
									
										23
									
								
								backend/src/util/async.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								backend/src/util/async.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| /** | ||||
|  * Replace all occurrences of regex in str with the result of asyncFn called with the matching snippet and each of | ||||
|  * the parts matched by a group in the regex as arguments. | ||||
|  * | ||||
|  * @param str The string where to replace the occurrences | ||||
|  * @param regex | ||||
|  * @param replacementFn | ||||
|  */ | ||||
| export async function replaceAsync(str: string, regex: RegExp, replacementFn: (match: string, ...args: string[]) => Promise<string>) { | ||||
|     const promises: Promise<string>[] = []; | ||||
| 
 | ||||
|     // First run through matches: add all Promises resulting from the replacement function
 | ||||
|     str.replace(regex, (full, ...args) => { | ||||
|         promises.push(replacementFn(full, ...args)); | ||||
|         return full; | ||||
|     }); | ||||
| 
 | ||||
|     // Wait for the replacements to get loaded. Reverse them so when popping them, we work in a FIFO manner.
 | ||||
|     const replacements: string[] = await Promise.all(promises); | ||||
| 
 | ||||
|     // Second run through matches: Replace them by their previously computed replacements.
 | ||||
|     return str.replace(regex, () => replacements.pop()!); | ||||
| } | ||||
|  | @ -1,5 +1,9 @@ | |||
| const PREFIX = 'DWENGO_'; | ||||
| const DB_PREFIX = PREFIX + 'DB_'; | ||||
| const IDP_PREFIX = PREFIX + 'AUTH_'; | ||||
| const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_'; | ||||
| const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; | ||||
| const CORS_PREFIX = PREFIX + 'CORS_'; | ||||
| 
 | ||||
| type EnvVar = { key: string; required?: boolean; defaultValue?: any }; | ||||
| 
 | ||||
|  | @ -11,6 +15,18 @@ export const EnvVars: { [key: string]: EnvVar } = { | |||
|     DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, | ||||
|     DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, | ||||
|     DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, | ||||
|     LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' }, | ||||
|     FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' }, | ||||
|     UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: 'u_' }, | ||||
|     IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true }, | ||||
|     IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', required: true }, | ||||
|     IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, | ||||
|     IdpTeacherUrl: { key: TEACHER_IDP_PREFIX + 'URL', required: true }, | ||||
|     IdpTeacherClientId: { key: TEACHER_IDP_PREFIX + 'CLIENT_ID', required: true }, | ||||
|     IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, | ||||
|     IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' }, | ||||
|     CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' }, | ||||
|     CorsAllowedHeaders: { key: CORS_PREFIX + 'ALLOWED_HEADERS', defaultValue: 'Authorization,Content-Type' }, | ||||
| } as const; | ||||
| 
 | ||||
| /** | ||||
|  | @ -36,9 +52,7 @@ export function getNumericEnvVar(envVar: EnvVar): number { | |||
|     const valueString = getEnvVar(envVar); | ||||
|     const value = parseInt(valueString); | ||||
|     if (isNaN(value)) { | ||||
|         throw new Error( | ||||
|             `Invalid value for environment variable ${envVar.key}: ${valueString}. Expected a number.` | ||||
|         ); | ||||
|         throw new Error(`Invalid value for environment variable ${envVar.key}: ${valueString}. Expected a number.`); | ||||
|     } else { | ||||
|         return value; | ||||
|     } | ||||
|  |  | |||
							
								
								
									
										26
									
								
								backend/src/util/links.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								backend/src/util/links.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| import { LearningObjectIdentifier } from '../interfaces/learning-content'; | ||||
| 
 | ||||
| export function isValidHttpUrl(url: string): boolean { | ||||
|     try { | ||||
|         const parsedUrl = new URL(url, 'http://test.be'); | ||||
|         return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'; | ||||
|     } catch (e) { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifier) { | ||||
|     let url = `/learningObject/${learningObjectId.hruid}/html?language=${learningObjectId.language}`; | ||||
|     if (learningObjectId.version) { | ||||
|         url += `&version=${learningObjectId.version}`; | ||||
|     } | ||||
|     return url; | ||||
| } | ||||
| 
 | ||||
| export function getUrlStringForLearningObjectHTML(learningObjectIdentifier: LearningObjectIdentifier): string { | ||||
|     let url = `/learningObject/${learningObjectIdentifier.hruid}/html?language=${learningObjectIdentifier.language}`; | ||||
|     if (learningObjectIdentifier.version) { | ||||
|         url += `&version=${learningObjectIdentifier.version}`; | ||||
|     } | ||||
|     return url; | ||||
| } | ||||
|  | @ -2,6 +2,9 @@ import fs from 'fs'; | |||
| import path from 'path'; | ||||
| import yaml from 'js-yaml'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import { getLogger, Logger } from '../logging/initalize.js'; | ||||
| 
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
| export function loadTranslations<T>(language: string): T { | ||||
|     try { | ||||
|  | @ -9,15 +12,8 @@ export function loadTranslations<T>(language: string): T { | |||
|         const yamlFile = fs.readFileSync(filePath, 'utf8'); | ||||
|         return yaml.load(yamlFile) as T; | ||||
|     } catch (error) { | ||||
|         console.error( | ||||
|             `Cannot load translation for ${language}, fallen back to dutch` | ||||
|         ); | ||||
|         console.error(error); | ||||
|         const fallbackPath = path.join( | ||||
|             process.cwd(), | ||||
|             '_i18n', | ||||
|             `${FALLBACK_LANG}.yml` | ||||
|         ); | ||||
|         logger.warn(`Cannot load translation for ${language}, fallen back to dutch`, error); | ||||
|         const fallbackPath = path.join(process.cwd(), '_i18n', `${FALLBACK_LANG}.yml`); | ||||
|         return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T; | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl