Merge branch 'dev' into feat/home-data
This commit is contained in:
		
						commit
						1531fa36fe
					
				
					 20 changed files with 931 additions and 35 deletions
				
			
		|  | @ -3,14 +3,16 @@ import { initORM } from './orm.js'; | |||
| import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | ||||
| 
 | ||||
| import themeRoutes from './routes/themes.js'; | ||||
| import learningPathRoutes from './routes/learningPaths.js'; | ||||
| import learningObjectRoutes from './routes/learningObjects.js'; | ||||
| 
 | ||||
| import studentRouter from './routes/student'; | ||||
| import groupRouter from './routes/group'; | ||||
| import assignmentRouter from './routes/assignment'; | ||||
| import submissionRouter from './routes/submission'; | ||||
| import classRouter from './routes/class'; | ||||
| import questionRouter from './routes/question'; | ||||
| import loginRouter from './routes/login'; | ||||
| import studentRouter from './routes/student.js'; | ||||
| import groupRouter from './routes/group.js'; | ||||
| import assignmentRouter from './routes/assignment.js'; | ||||
| import submissionRouter from './routes/submission.js'; | ||||
| import classRouter from './routes/class.js'; | ||||
| import questionRouter from './routes/question.js'; | ||||
| import loginRouter from './routes/login.js'; | ||||
| 
 | ||||
| const app: Express = express(); | ||||
| const port: string | number = getNumericEnvVar(EnvVars.Port); | ||||
|  | @ -32,6 +34,8 @@ app.use('/question', questionRouter); | |||
| app.use('/login', loginRouter); | ||||
| 
 | ||||
| app.use('/theme', themeRoutes); | ||||
| app.use('/learningPath', learningPathRoutes); | ||||
| app.use('/learningObject', learningObjectRoutes); | ||||
| 
 | ||||
| async function startServer() { | ||||
|     await initORM(); | ||||
|  |  | |||
							
								
								
									
										10
									
								
								backend/src/config.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/config.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| // Can be placed in dotenv but found it redundant
 | ||||
| 
 | ||||
| // Import dotenv from "dotenv";
 | ||||
| 
 | ||||
| // Load .env file
 | ||||
| // Dotenv.config();
 | ||||
| 
 | ||||
| export const DWENGO_API_BASE = 'https://dwengo.org/backend/api'; | ||||
| 
 | ||||
| export const FALLBACK_LANG = 'nl'; | ||||
							
								
								
									
										60
									
								
								backend/src/controllers/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								backend/src/controllers/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { | ||||
|     getLearningObjectById, | ||||
|     getLearningObjectIdsFromPath, | ||||
|     getLearningObjectsFromPath, | ||||
| } from '../services/learningObjects.js'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import { FilteredLearningObject } from '../interfaces/learningPath'; | ||||
| 
 | ||||
| export async function getAllLearningObjects( | ||||
|     req: Request, | ||||
|     res: Response | ||||
| ): Promise<void> { | ||||
|     try { | ||||
|         const hruid = req.query.hruid as string; | ||||
|         const full = req.query.full === 'true'; | ||||
|         const language = (req.query.language as string) || FALLBACK_LANG; | ||||
| 
 | ||||
|         if (!hruid) { | ||||
|             res.status(400).json({ error: 'HRUID query is required.' }); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let learningObjects: FilteredLearningObject[] | string[]; | ||||
|         if (full) { | ||||
|             learningObjects = await getLearningObjectsFromPath(hruid, language); | ||||
|         } else { | ||||
|             learningObjects = await getLearningObjectIdsFromPath( | ||||
|                 hruid, | ||||
|                 language | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         res.json(learningObjects); | ||||
|     } catch (error) { | ||||
|         console.error('Error fetching learning objects:', error); | ||||
|         res.status(500).json({ error: 'Internal server error' }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export async function getLearningObject( | ||||
|     req: Request, | ||||
|     res: Response | ||||
| ): Promise<void> { | ||||
|     try { | ||||
|         const { hruid } = req.params; | ||||
|         const language = (req.query.language as string) || FALLBACK_LANG; | ||||
| 
 | ||||
|         if (!hruid) { | ||||
|             res.status(400).json({ error: 'HRUID parameter is required.' }); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const learningObject = await getLearningObjectById(hruid, language); | ||||
|         res.json(learningObject); | ||||
|     } catch (error) { | ||||
|         console.error('Error fetching learning object:', error); | ||||
|         res.status(500).json({ error: 'Internal server error' }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										62
									
								
								backend/src/controllers/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								backend/src/controllers/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { themes } from '../data/themes.js'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import { | ||||
|     fetchLearningPaths, | ||||
|     searchLearningPaths, | ||||
| } from '../services/learningPaths.js'; | ||||
| /** | ||||
|  * Fetch learning paths based on query parameters. | ||||
|  */ | ||||
| export async function getLearningPaths( | ||||
|     req: Request, | ||||
|     res: Response | ||||
| ): Promise<void> { | ||||
|     try { | ||||
|         const hruids = req.query.hruid; | ||||
|         const themeKey = req.query.theme as string; | ||||
|         const searchQuery = req.query.search as string; | ||||
|         const language = (req.query.language as string) || FALLBACK_LANG; | ||||
| 
 | ||||
|         let hruidList; | ||||
| 
 | ||||
|         if (hruids) { | ||||
|             hruidList = Array.isArray(hruids) | ||||
|                 ? hruids.map(String) | ||||
|                 : [String(hruids)]; | ||||
|         } else if (themeKey) { | ||||
|             const theme = themes.find((t) => { | ||||
|                 return t.title === themeKey; | ||||
|             }); | ||||
|             if (theme) { | ||||
|                 hruidList = theme.hruids; | ||||
|             } else { | ||||
|                 res.status(404).json({ | ||||
|                     error: `Theme "${themeKey}" not found.`, | ||||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
|         } else if (searchQuery) { | ||||
|             const searchResults = await searchLearningPaths( | ||||
|                 searchQuery, | ||||
|                 language | ||||
|             ); | ||||
|             res.json(searchResults); | ||||
|             return; | ||||
|         } else { | ||||
|             hruidList = themes.flatMap((theme) => { | ||||
|                 return theme.hruids; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         const learningPaths = await fetchLearningPaths( | ||||
|             hruidList, | ||||
|             language, | ||||
|             `HRUIDs: ${hruidList.join(', ')}` | ||||
|         ); | ||||
|         res.json(learningPaths.data); | ||||
|     } catch (error) { | ||||
|         console.error('❌ Unexpected error fetching learning paths:', error); | ||||
|         res.status(500).json({ error: 'Internal server error' }); | ||||
|     } | ||||
| } | ||||
|  | @ -1,6 +1,7 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { themes } from '../data/themes.js'; | ||||
| import { loadTranslations } from "../util/translationHelper.js"; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| 
 | ||||
| interface Translations { | ||||
|     curricula_page: { | ||||
|  | @ -8,13 +9,9 @@ interface Translations { | |||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /themes → Haalt de lijst met thema's op inclusief vertalingen | ||||
|  */ | ||||
| export function getThemes(req: Request, res: Response) { | ||||
|     const language = (req.query.language as string)?.toLowerCase() || 'nl'; | ||||
|     const translations = loadTranslations<Translations>(language); | ||||
| 
 | ||||
|     const themeList = themes.map((theme) => { | ||||
|         return { | ||||
|             key: theme.title, | ||||
|  | @ -28,9 +25,6 @@ export function getThemes(req: Request, res: Response) { | |||
|     res.json(themeList); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * GET /themes/:theme → Geeft de HRUIDs terug voor een specifiek thema | ||||
|  */ | ||||
| export function getThemeByTitle(req: Request, res: Response) { | ||||
|     const themeKey = req.params.theme; | ||||
|     const theme = themes.find((t) => { | ||||
|  | @ -40,6 +34,6 @@ export function getThemeByTitle(req: Request, res: Response) { | |||
|     if (theme) { | ||||
|         res.json(theme.hruids); | ||||
|     } else { | ||||
|         res.status(404).json({ error: 'Thema niet gevonden' }); | ||||
|         res.status(404).json({ error: 'Theme not found' }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										98
									
								
								backend/src/interfaces/learningPath.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								backend/src/interfaces/learningPath.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,98 @@ | |||
| export interface Transition { | ||||
|     default: boolean; | ||||
|     _id: string; | ||||
|     next: { | ||||
|         _id: string; | ||||
|         hruid: string; | ||||
|         version: number; | ||||
|         language: string; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObjectNode { | ||||
|     _id: string; | ||||
|     learningobject_hruid: string; | ||||
|     version: number; | ||||
|     language: string; | ||||
|     start_node?: boolean; | ||||
|     transitions: Transition[]; | ||||
|     created_at: string; | ||||
|     updatedAt: string; | ||||
| } | ||||
| 
 | ||||
| export interface LearningPath { | ||||
|     _id: string; | ||||
|     language: string; | ||||
|     hruid: string; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     image?: string; // Image might be missing, so it's optional
 | ||||
|     num_nodes: number; | ||||
|     num_nodes_left: number; | ||||
|     nodes: LearningObjectNode[]; | ||||
|     keywords: string; | ||||
|     target_ages: number[]; | ||||
|     min_age: number; | ||||
|     max_age: number; | ||||
|     __order: number; | ||||
| } | ||||
| 
 | ||||
| export interface EducationalGoal { | ||||
|     source: string; | ||||
|     id: string; | ||||
| } | ||||
| 
 | ||||
| export interface ReturnValue { | ||||
|     callback_url: string; | ||||
|     callback_schema: Record<string, any>; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObjectMetadata { | ||||
|     _id: string; | ||||
|     uuid: string; | ||||
|     hruid: string; | ||||
|     version: number; | ||||
|     language: string; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     difficulty: number; | ||||
|     estimated_time: number; | ||||
|     available: boolean; | ||||
|     teacher_exclusive: boolean; | ||||
|     educational_goals: EducationalGoal[]; | ||||
|     keywords: string[]; | ||||
|     target_ages: number[]; | ||||
|     content_type: string; // Markdown, image, etc.
 | ||||
|     content_location?: string; | ||||
|     skos_concepts?: string[]; | ||||
|     return_value?: ReturnValue; | ||||
| } | ||||
| 
 | ||||
| export interface FilteredLearningObject { | ||||
|     key: string; | ||||
|     _id: string; | ||||
|     uuid: string; | ||||
|     version: number; | ||||
|     title: string; | ||||
|     htmlUrl: string; | ||||
|     language: string; | ||||
|     difficulty: number; | ||||
|     estimatedTime: number; | ||||
|     available: boolean; | ||||
|     teacherExclusive: boolean; | ||||
|     educationalGoals: EducationalGoal[]; | ||||
|     keywords: string[]; | ||||
|     description: string; | ||||
|     targetAges: number[]; | ||||
|     contentType: string; | ||||
|     contentLocation?: string; | ||||
|     skosConcepts?: string[]; | ||||
|     returnValue?: ReturnValue; | ||||
| } | ||||
| 
 | ||||
| export interface LearningPathResponse { | ||||
|     success: boolean; | ||||
|     source: string; | ||||
|     data: LearningPath[] | null; | ||||
|     message?: string; | ||||
| } | ||||
							
								
								
									
										27
									
								
								backend/src/routes/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								backend/src/routes/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| import express from 'express'; | ||||
| import { | ||||
|     getAllLearningObjects, | ||||
|     getLearningObject, | ||||
| } from '../controllers/learningObjects.js'; | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // DWENGO learning objects
 | ||||
| 
 | ||||
| // Queries: hruid(path), full, language
 | ||||
| // Route to fetch list of learning objects based on hruid of learning path
 | ||||
| 
 | ||||
| // Route 1: list of object hruids
 | ||||
| // Example 1: http://localhost:3000/learningObject?hruid=un_artificiele_intelligentie
 | ||||
| 
 | ||||
| // Route 2: list of object data
 | ||||
| // Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie
 | ||||
| router.get('/', getAllLearningObjects); | ||||
| 
 | ||||
| // Parameter: hruid of learning object
 | ||||
| // Query: language
 | ||||
| // Route to fetch data of one learning object based on its hruid
 | ||||
| // Example: http://localhost:3000/learningObject/un_ai7
 | ||||
| router.get('/:hruid', getLearningObject); | ||||
| 
 | ||||
| export default router; | ||||
							
								
								
									
										27
									
								
								backend/src/routes/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								backend/src/routes/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| import express from 'express'; | ||||
| import { getLearningPaths } from '../controllers/learningPaths.js'; | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // DWENGO learning paths
 | ||||
| 
 | ||||
| // Route 1: no query
 | ||||
| // Fetch all learning paths
 | ||||
| // Example 1: http://localhost:3000/learningPath
 | ||||
| 
 | ||||
| // Unified route for fetching learning paths
 | ||||
| // Route 2: Query: hruid (list), language
 | ||||
| // Fetch learning paths based on hruid list
 | ||||
| // Example 2: http://localhost:3000/learningPath?hruid=pn_werking&hruid=art1
 | ||||
| 
 | ||||
| // Query: search, language
 | ||||
| // Route to fetch learning paths based on a searchterm
 | ||||
| // Example 3: http://localhost:3000/learningPath?search=robot
 | ||||
| 
 | ||||
| // Query: theme, anguage
 | ||||
| // Route to fetch learning paths based on a theme
 | ||||
| // Example: http://localhost:3000/learningPath?theme=kiks
 | ||||
| 
 | ||||
| router.get('/', getLearningPaths); | ||||
| 
 | ||||
| export default router; | ||||
|  | @ -3,7 +3,12 @@ import { getThemes, getThemeByTitle } from '../controllers/themes.js'; | |||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // Query: language
 | ||||
| //  Route to fetch list of {key, title, description, image} themes in their respective language
 | ||||
| router.get('/', getThemes); | ||||
| 
 | ||||
| // Arg: theme (key)
 | ||||
| //  Route to fetch list of hruids based on theme
 | ||||
| router.get('/:theme', getThemeByTitle); | ||||
| 
 | ||||
| export default router; | ||||
|  |  | |||
							
								
								
									
										134
									
								
								backend/src/services/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								backend/src/services/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,134 @@ | |||
| import { DWENGO_API_BASE } from '../config.js'; | ||||
| import { fetchWithLogging } from '../util/apiHelper.js'; | ||||
| import { | ||||
|     FilteredLearningObject, | ||||
|     LearningObjectMetadata, | ||||
|     LearningObjectNode, | ||||
|     LearningPathResponse, | ||||
| } from '../interfaces/learningPath.js'; | ||||
| import { fetchLearningPaths } from './learningPaths.js'; | ||||
| 
 | ||||
| function filterData( | ||||
|     data: LearningObjectMetadata, | ||||
|     htmlUrl: string | ||||
| ): FilteredLearningObject { | ||||
|     return { | ||||
|         key: data.hruid, // Hruid learningObject (not path)
 | ||||
|         _id: data._id, | ||||
|         uuid: data.uuid, | ||||
|         version: data.version, | ||||
|         title: data.title, | ||||
|         htmlUrl, // Url to fetch html content
 | ||||
|         language: data.language, | ||||
|         difficulty: data.difficulty, | ||||
|         estimatedTime: data.estimated_time, | ||||
|         available: data.available, | ||||
|         teacherExclusive: data.teacher_exclusive, | ||||
|         educationalGoals: data.educational_goals, // List with learningObjects
 | ||||
|         keywords: data.keywords, // For search
 | ||||
|         description: data.description, // For search (not an actual description)
 | ||||
|         targetAges: data.target_ages, | ||||
|         contentType: data.content_type, // Markdown, image, audio, etc.
 | ||||
|         contentLocation: data.content_location, // If content type extern
 | ||||
|         skosConcepts: data.skos_concepts, | ||||
|         returnValue: data.return_value, // Callback response information
 | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Fetches a single learning object by its HRUID | ||||
|  */ | ||||
| export async function getLearningObjectById( | ||||
|     hruid: string, | ||||
|     language: string | ||||
| ): Promise<FilteredLearningObject | null> { | ||||
|     const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; | ||||
|     const metadata = await fetchWithLogging<LearningObjectMetadata>( | ||||
|         metadataUrl, | ||||
|         `Metadata for Learning Object HRUID "${hruid}" (language ${language})` | ||||
|     ); | ||||
| 
 | ||||
|     if (!metadata) { | ||||
|         console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`; | ||||
|     return filterData(metadata, htmlUrl); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Generic function to fetch learning objects (full data or just HRUIDs) | ||||
|  */ | ||||
| async function fetchLearningObjects( | ||||
|     hruid: string, | ||||
|     full: boolean, | ||||
|     language: string | ||||
| ): Promise<FilteredLearningObject[] | string[]> { | ||||
|     try { | ||||
|         const learningPathResponse: LearningPathResponse = | ||||
|             await fetchLearningPaths( | ||||
|                 [hruid], | ||||
|                 language, | ||||
|                 `Learning path for HRUID "${hruid}"` | ||||
|             ); | ||||
| 
 | ||||
|         if ( | ||||
|             !learningPathResponse.success || | ||||
|             !learningPathResponse.data?.length | ||||
|         ) { | ||||
|             console.error( | ||||
|                 `⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.` | ||||
|             ); | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         const nodes: LearningObjectNode[] = learningPathResponse.data[0].nodes; | ||||
| 
 | ||||
|         if (!full) { | ||||
|             return nodes.map((node) => { | ||||
|                 return node.learningobject_hruid; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return await Promise.all( | ||||
|             nodes.map(async (node) => { | ||||
|                 return getLearningObjectById( | ||||
|                     node.learningobject_hruid, | ||||
|                     language | ||||
|                 ); | ||||
|             }) | ||||
|         ).then((objects) => { | ||||
|             return objects.filter((obj): obj is FilteredLearningObject => { | ||||
|                 return obj !== null; | ||||
|             }); | ||||
|         }); | ||||
|     } catch (error) { | ||||
|         console.error('❌ Error fetching learning objects:', error); | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Fetch full learning object data (metadata) | ||||
|  */ | ||||
| export async function getLearningObjectsFromPath( | ||||
|     hruid: string, | ||||
|     language: string | ||||
| ): Promise<FilteredLearningObject[]> { | ||||
|     return (await fetchLearningObjects( | ||||
|         hruid, | ||||
|         true, | ||||
|         language | ||||
|     )) as FilteredLearningObject[]; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Fetch only learning object HRUIDs | ||||
|  */ | ||||
| export async function getLearningObjectIdsFromPath( | ||||
|     hruid: string, | ||||
|     language: string | ||||
| ): Promise<string[]> { | ||||
|     return (await fetchLearningObjects(hruid, false, language)) as string[]; | ||||
| } | ||||
							
								
								
									
										61
									
								
								backend/src/services/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								backend/src/services/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | |||
| import { fetchWithLogging } from '../util/apiHelper.js'; | ||||
| import { DWENGO_API_BASE } from '../config.js'; | ||||
| import { | ||||
|     LearningPath, | ||||
|     LearningPathResponse, | ||||
| } from '../interfaces/learningPath.js'; | ||||
| 
 | ||||
| export async function fetchLearningPaths( | ||||
|     hruids: string[], | ||||
|     language: string, | ||||
|     source: string | ||||
| ): Promise<LearningPathResponse> { | ||||
|     if (hruids.length === 0) { | ||||
|         return { | ||||
|             success: false, | ||||
|             source, | ||||
|             data: null, | ||||
|             message: `No HRUIDs provided for ${source}.`, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; | ||||
|     const params = { pathIdList: JSON.stringify({ hruids }), language }; | ||||
| 
 | ||||
|     const learningPaths = await fetchWithLogging<LearningPath[]>( | ||||
|         apiUrl, | ||||
|         `Learning paths for ${source}`, | ||||
|         params | ||||
|     ); | ||||
| 
 | ||||
|     if (!learningPaths || learningPaths.length === 0) { | ||||
|         console.error(`⚠️ WARNING: No learning paths found for ${source}.`); | ||||
|         return { | ||||
|             success: false, | ||||
|             source, | ||||
|             data: [], | ||||
|             message: `No learning paths found for ${source}.`, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         success: true, | ||||
|         source, | ||||
|         data: learningPaths, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export async function searchLearningPaths( | ||||
|     query: string, | ||||
|     language: string | ||||
| ): Promise<LearningPath[]> { | ||||
|     const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; | ||||
|     const params = { all: query, language }; | ||||
| 
 | ||||
|     const searchResults = await fetchWithLogging<LearningPath[]>( | ||||
|         apiUrl, | ||||
|         `Search learning paths with query "${query}"`, | ||||
|         params | ||||
|     ); | ||||
|     return searchResults ?? []; | ||||
| } | ||||
							
								
								
									
										43
									
								
								backend/src/util/apiHelper.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								backend/src/util/apiHelper.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| import axios, { AxiosRequestConfig } from 'axios'; | ||||
| 
 | ||||
| // !!!! when logger is done -> change
 | ||||
| 
 | ||||
| /** | ||||
|  * Utility function to fetch data from an API endpoint with error handling. | ||||
|  * Logs errors but does NOT throw exceptions to keep the system running. | ||||
|  * | ||||
|  * @param url The API endpoint to fetch from. | ||||
|  * @param description A short description of what is being fetched (for logging). | ||||
|  * @param params | ||||
|  * @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> | ||||
| ): Promise<T | null> { | ||||
|     try { | ||||
|         const config: AxiosRequestConfig = params ? { params } : {}; | ||||
| 
 | ||||
|         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}".` | ||||
|                 ); | ||||
|             } else { | ||||
|                 console.error( | ||||
|                     `❌ 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 | ||||
|             ); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl