feat(backend): Added endpoint to fetch HTML version of learning object (from Dwengo backend)
Also refactored a bit to make this easier.
This commit is contained in:
		
							parent
							
								
									770c5c9879
								
							
						
					
					
						commit
						18ee991ce3
					
				
					 16 changed files with 264 additions and 178 deletions
				
			
		|  | @ -3,8 +3,8 @@ import { initORM } from './orm.js'; | ||||||
| import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | ||||||
| 
 | 
 | ||||||
| import themeRoutes from './routes/themes.js'; | import themeRoutes from './routes/themes.js'; | ||||||
| import learningPathRoutes from './routes/learningPaths.js'; | import learningPathRoutes from './routes/learning-paths.js'; | ||||||
| import learningObjectRoutes from './routes/learningObjects.js'; | import learningObjectRoutes from './routes/learning-objects.js'; | ||||||
| 
 | 
 | ||||||
| import studentRouter from './routes/student.js'; | import studentRouter from './routes/student.js'; | ||||||
| import groupRouter from './routes/group.js'; | import groupRouter from './routes/group.js'; | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ | ||||||
| // Load .env file
 | // Load .env file
 | ||||||
| // Dotenv.config();
 | // Dotenv.config();
 | ||||||
| 
 | 
 | ||||||
| export const DWENGO_API_BASE = 'https://dwengo.org/backend/api'; | import {EnvVars, getEnvVar} from "./util/envvars"; | ||||||
| 
 | 
 | ||||||
| export const FALLBACK_LANG = 'nl'; | export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); | ||||||
|  | 
 | ||||||
|  | export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); | ||||||
|  |  | ||||||
							
								
								
									
										62
									
								
								backend/src/controllers/learning-objects.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								backend/src/controllers/learning-objects.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import { FALLBACK_LANG } from '../config.js'; | ||||||
|  | import {FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier} from '../interfaces/learning-content'; | ||||||
|  | import learningObjectService from "../services/learning-content/learning-object-service"; | ||||||
|  | import {EnvVars, getEnvVar} from "../util/envvars"; | ||||||
|  | import {Language} from "../entities/content/language"; | ||||||
|  | import {BadRequestException} from "../exceptions"; | ||||||
|  | 
 | ||||||
|  | function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | ||||||
|  |     if (!req.params.hruid) { | ||||||
|  |         throw new BadRequestException("HRUID is required."); | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |         hruid: req.params.hruid as string, | ||||||
|  |         language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language, | ||||||
|  |         version: req.query.version as string | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentifier { | ||||||
|  |     if (!req.query.hruid) { | ||||||
|  |         throw new BadRequestException("HRUID is required."); | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |         hruid: req.params.hruid as string, | ||||||
|  |         language: (req.query.language as Language) || FALLBACK_LANG | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getAllLearningObjects( | ||||||
|  |     req: Request, | ||||||
|  |     res: Response | ||||||
|  | ): Promise<void> { | ||||||
|  |     const learningPathId = getLearningPathIdentifierFromRequest(req); | ||||||
|  |     const full = req.query.full; | ||||||
|  | 
 | ||||||
|  |     let learningObjects: FilteredLearningObject[] | string[]; | ||||||
|  |     if (full) { | ||||||
|  |         learningObjects = await learningObjectService.getLearningObjectsFromPath(learningPathId); | ||||||
|  |     } else { | ||||||
|  |         learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     res.json(learningObjects); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getLearningObject( | ||||||
|  |     req: Request, | ||||||
|  |     res: Response | ||||||
|  | ): Promise<void> { | ||||||
|  |     const learningObjectId = getLearningObjectIdentifierFromRequest(req); | ||||||
|  | 
 | ||||||
|  |     const learningObject = await learningObjectService.getLearningObjectById(learningObjectId); | ||||||
|  |     res.json(learningObject); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getLearningObjectHTML(req: Request, res: Response): Promise<void> { | ||||||
|  |     const learningObjectId = getLearningObjectIdentifierFromRequest(req); | ||||||
|  | 
 | ||||||
|  |     const learningObject = await learningObjectService.getLearningObjectHTML(learningObjectId); | ||||||
|  |     res.send(learningObject); | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								backend/src/controllers/learning-paths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								backend/src/controllers/learning-paths.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | ||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import { themes } from '../data/themes.js'; | ||||||
|  | import { FALLBACK_LANG } from '../config.js'; | ||||||
|  | import learningPathService from "../services/learning-content/learning-path-service"; | ||||||
|  | import {NotFoundException} from "../exceptions"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Fetch learning paths based on query parameters. | ||||||
|  |  */ | ||||||
|  | export async function getLearningPaths( | ||||||
|  |     req: Request, | ||||||
|  |     res: Response | ||||||
|  | ): Promise<void> { | ||||||
|  |     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 { | ||||||
|  |             throw new NotFoundException(`Theme "${themeKey}" not found.`); | ||||||
|  |         } | ||||||
|  |     } else if (searchQuery) { | ||||||
|  |         const searchResults = await learningPathService.searchLearningPaths( | ||||||
|  |             searchQuery, | ||||||
|  |             language | ||||||
|  |         ); | ||||||
|  |         res.json(searchResults); | ||||||
|  |         return; | ||||||
|  |     } else { | ||||||
|  |         hruidList = themes.flatMap((theme) => { | ||||||
|  |             return theme.hruids; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const learningPaths = await learningPathService.fetchLearningPaths( | ||||||
|  |         hruidList, | ||||||
|  |         language, | ||||||
|  |         `HRUIDs: ${hruidList.join(', ')}` | ||||||
|  |     ); | ||||||
|  |     res.json(learningPaths.data); | ||||||
|  | } | ||||||
|  | @ -1,56 +0,0 @@ | ||||||
| import { Request, Response } from 'express'; |  | ||||||
| import { FALLBACK_LANG } from '../config.js'; |  | ||||||
| import { FilteredLearningObject } from '../interfaces/learningContent'; |  | ||||||
| import learningObjectService from "../services/learning-content/learning-object-service"; |  | ||||||
| 
 |  | ||||||
| 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 learningObjectService.getLearningObjectsFromPath(hruid, language); |  | ||||||
|         } else { |  | ||||||
|             learningObjects = await learningObjectService.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 learningObjectService.getLearningObjectById(hruid, language); |  | ||||||
|         res.json(learningObject); |  | ||||||
|     } catch (error) { |  | ||||||
|         console.error('Error fetching learning object:', error); |  | ||||||
|         res.status(500).json({ error: 'Internal server error' }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,59 +0,0 @@ | ||||||
| import { Request, Response } from 'express'; |  | ||||||
| import { themes } from '../data/themes.js'; |  | ||||||
| import { FALLBACK_LANG } from '../config.js'; |  | ||||||
| import learningPathService from "../services/learning-content/learning-path-service"; |  | ||||||
| /** |  | ||||||
|  * 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 learningPathService.searchLearningPaths( |  | ||||||
|                 searchQuery, |  | ||||||
|                 language |  | ||||||
|             ); |  | ||||||
|             res.json(searchResults); |  | ||||||
|             return; |  | ||||||
|         } else { |  | ||||||
|             hruidList = themes.flatMap((theme) => { |  | ||||||
|                 return theme.hruids; |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const learningPaths = await learningPathService.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' }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										21
									
								
								backend/src/exceptions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								backend/src/exceptions.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 400 Bad Request | ||||||
|  |  */ | ||||||
|  | export class BadRequestException extends Error { | ||||||
|  |     public status = 400; | ||||||
|  | 
 | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(error); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 404 Not Found | ||||||
|  |  */ | ||||||
|  | export class NotFoundException extends Error { | ||||||
|  |     public status = 404; | ||||||
|  | 
 | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(error); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,3 +1,5 @@ | ||||||
|  | import {Language} from "../entities/content/language"; | ||||||
|  | 
 | ||||||
| export interface Transition { | export interface Transition { | ||||||
|     default: boolean; |     default: boolean; | ||||||
|     _id: string; |     _id: string; | ||||||
|  | @ -9,6 +11,12 @@ export interface Transition { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface LearningObjectIdentifier { | ||||||
|  |     hruid: string; | ||||||
|  |     language: Language; | ||||||
|  |     version?: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export interface LearningObjectNode { | export interface LearningObjectNode { | ||||||
|     _id: string; |     _id: string; | ||||||
|     learningobject_hruid: string; |     learningobject_hruid: string; | ||||||
|  | @ -20,7 +28,7 @@ export interface LearningObjectNode { | ||||||
|     updatedAt: string; |     updatedAt: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface LearningContent { | export interface LearningPath { | ||||||
|     _id: string; |     _id: string; | ||||||
|     language: string; |     language: string; | ||||||
|     hruid: string; |     hruid: string; | ||||||
|  | @ -37,6 +45,11 @@ export interface LearningContent { | ||||||
|     __order: number; |     __order: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface LearningPathIdentifier { | ||||||
|  |     hruid: string; | ||||||
|  |     language: Language; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export interface EducationalGoal { | export interface EducationalGoal { | ||||||
|     source: string; |     source: string; | ||||||
|     id: string; |     id: string; | ||||||
|  | @ -93,6 +106,6 @@ export interface FilteredLearningObject { | ||||||
| export interface LearningPathResponse { | export interface LearningPathResponse { | ||||||
|     success: boolean; |     success: boolean; | ||||||
|     source: string; |     source: string; | ||||||
|     data: LearningContent[] | null; |     data: LearningPath[] | null; | ||||||
|     message?: string; |     message?: string; | ||||||
| } | } | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { | import { | ||||||
|     getAllLearningObjects, |     getAllLearningObjects, | ||||||
|     getLearningObject, |     getLearningObject, getLearningObjectHTML, | ||||||
| } from '../controllers/learningObjects.js'; | } from '../controllers/learning-objects.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | @ -24,4 +24,10 @@ router.get('/', getAllLearningObjects); | ||||||
| // Example: http://localhost:3000/learningObject/un_ai7
 | // Example: http://localhost:3000/learningObject/un_ai7
 | ||||||
| router.get('/:hruid', getLearningObject); | router.get('/:hruid', getLearningObject); | ||||||
| 
 | 
 | ||||||
|  | // Parameter: hruid of learning object
 | ||||||
|  | // Query: language, version (optional)
 | ||||||
|  | // Route to fetch the HTML rendering of one learning object based on its hruid.
 | ||||||
|  | // Example: http://localhost:3000/learningObject/un_ai7/html
 | ||||||
|  | router.get('/:hruid/html', getLearningObjectHTML); | ||||||
|  | 
 | ||||||
| export default router; | export default router; | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { getLearningPaths } from '../controllers/learningPaths.js'; | import { getLearningPaths } from '../controllers/learning-paths.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { DWENGO_API_BASE } from '../../../config.js'; | import { DWENGO_API_BASE } from '../../../config.js'; | ||||||
| import { fetchWithLogging } from '../../../util/apiHelper.js'; | import { fetchWithLogging } from '../../../util/apiHelper.js'; | ||||||
| import { | import { | ||||||
|     FilteredLearningObject, |     FilteredLearningObject, LearningObjectIdentifier, | ||||||
|     LearningObjectMetadata, |     LearningObjectMetadata, | ||||||
|     LearningObjectNode, |     LearningObjectNode, LearningPathIdentifier, | ||||||
|     LearningPathResponse, |     LearningPathResponse, | ||||||
| } from '../../../interfaces/learningContent.js'; | } from '../../../interfaces/learning-content.js'; | ||||||
| import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; | import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; | ||||||
| import {LearningObjectProvider} from "../learning-object-provider"; | import {LearningObjectProvider} from "../learning-object-provider"; | ||||||
| 
 | 
 | ||||||
|  | @ -13,11 +13,9 @@ import {LearningObjectProvider} from "../learning-object-provider"; | ||||||
|  * Helper function to convert the learning object metadata retrieved from the API to a FilteredLearningObject which |  * Helper function to convert the learning object metadata retrieved from the API to a FilteredLearningObject which | ||||||
|  * our API should return. |  * our API should return. | ||||||
|  * @param data |  * @param data | ||||||
|  * @param htmlUrl |  | ||||||
|  */ |  */ | ||||||
| function filterData( | function filterData( | ||||||
|     data: LearningObjectMetadata, |     data: LearningObjectMetadata | ||||||
|     htmlUrl: string |  | ||||||
| ): FilteredLearningObject { | ): FilteredLearningObject { | ||||||
|     return { |     return { | ||||||
|         key: data.hruid, // Hruid learningObject (not path)
 |         key: data.hruid, // Hruid learningObject (not path)
 | ||||||
|  | @ -25,7 +23,7 @@ function filterData( | ||||||
|         uuid: data.uuid, |         uuid: data.uuid, | ||||||
|         version: data.version, |         version: data.version, | ||||||
|         title: data.title, |         title: data.title, | ||||||
|         htmlUrl, // Url to fetch html content
 |         htmlUrl: `/learningObject/${data.hruid}/html?language=${data.language}&version=${data.version}`, // Url to fetch html content
 | ||||||
|         language: data.language, |         language: data.language, | ||||||
|         difficulty: data.difficulty, |         difficulty: data.difficulty, | ||||||
|         estimatedTime: data.estimated_time, |         estimatedTime: data.estimated_time, | ||||||
|  | @ -43,19 +41,18 @@ function filterData( | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Generic helper function to fetch learning objects (full data or just HRUIDs) |  * Generic helper function to fetch all learning objects from a given path (full data or just HRUIDs) | ||||||
|  */ |  */ | ||||||
| async function fetchLearningObjects( | async function fetchLearningObjects( | ||||||
|     hruid: string, |     learningPathId: LearningPathIdentifier, | ||||||
|     full: boolean, |     full: boolean | ||||||
|     language: string |  | ||||||
| ): Promise<FilteredLearningObject[] | string[]> { | ): Promise<FilteredLearningObject[] | string[]> { | ||||||
|     try { |     try { | ||||||
|         const learningPathResponse: LearningPathResponse = |         const learningPathResponse: LearningPathResponse = | ||||||
|             await dwengoApiLearningPathProvider.fetchLearningPaths( |             await dwengoApiLearningPathProvider.fetchLearningPaths( | ||||||
|                 [hruid], |                 [learningPathId.hruid], | ||||||
|                 language, |                 learningPathId.language, | ||||||
|                 `Learning path for HRUID "${hruid}"` |                 `Learning path for HRUID "${learningPathId.hruid}"` | ||||||
|             ); |             ); | ||||||
| 
 | 
 | ||||||
|         if ( |         if ( | ||||||
|  | @ -63,7 +60,7 @@ async function fetchLearningObjects( | ||||||
|             !learningPathResponse.data?.length |             !learningPathResponse.data?.length | ||||||
|         ) { |         ) { | ||||||
|             console.error( |             console.error( | ||||||
|                 `⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.` |                 `⚠️ WARNING: Learning path "${learningPathId.hruid}" exists but contains no learning objects.` | ||||||
|             ); |             ); | ||||||
|             return []; |             return []; | ||||||
|         } |         } | ||||||
|  | @ -78,10 +75,10 @@ async function fetchLearningObjects( | ||||||
| 
 | 
 | ||||||
|         return await Promise.all( |         return await Promise.all( | ||||||
|             nodes.map(async (node) => { |             nodes.map(async (node) => { | ||||||
|                 return dwengoApiLearningObjectProvider.getLearningObjectById( |                 return dwengoApiLearningObjectProvider.getLearningObjectById({ | ||||||
|                     node.learningobject_hruid, |                     hruid: node.learningobject_hruid, | ||||||
|                     language |                     language: learningPathId.language | ||||||
|                 ); |                 }); | ||||||
|             }) |             }) | ||||||
|         ).then((objects) => { |         ).then((objects) => { | ||||||
|             return objects.filter((obj): obj is FilteredLearningObject => { |             return objects.filter((obj): obj is FilteredLearningObject => { | ||||||
|  | @ -99,46 +96,62 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | ||||||
|      * Fetches a single learning object by its HRUID |      * Fetches a single learning object by its HRUID | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectById( |     async getLearningObjectById( | ||||||
|         hruid: string, |         id: LearningObjectIdentifier | ||||||
|         language: string |  | ||||||
|     ): Promise<FilteredLearningObject | null> { |     ): Promise<FilteredLearningObject | null> { | ||||||
|         const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; |         let metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; | ||||||
|         const metadata = await fetchWithLogging<LearningObjectMetadata>( |         const metadata = await fetchWithLogging<LearningObjectMetadata>( | ||||||
|             metadataUrl, |             metadataUrl, | ||||||
|             `Metadata for Learning Object HRUID "${hruid}" (language ${language})` |             `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, | ||||||
|  |             { | ||||||
|  |                 params: id | ||||||
|  |             } | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         if (!metadata) { |         if (!metadata) { | ||||||
|             console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); |             console.error(`⚠️ WARNING: Learning object "${id.hruid}" not found.`); | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`; |         return filterData(metadata); | ||||||
|         return filterData(metadata, htmlUrl); |  | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Fetch full learning object data (metadata) |      * Fetch full learning object data (metadata) | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectsFromPath( |     async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> { | ||||||
|         hruid: string, |  | ||||||
|         language: string |  | ||||||
|     ): Promise<FilteredLearningObject[]> { |  | ||||||
|         return (await fetchLearningObjects( |         return (await fetchLearningObjects( | ||||||
|             hruid, |             id, | ||||||
|             true, |             true, | ||||||
|             language |  | ||||||
|         )) as FilteredLearningObject[]; |         )) as FilteredLearningObject[]; | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Fetch only learning object HRUIDs |      * Fetch only learning object HRUIDs | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectIdsFromPath( |     async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> { | ||||||
|         hruid: string, |         return (await fetchLearningObjects(id, false)) as string[]; | ||||||
|         language: string |     }, | ||||||
|     ): Promise<string[]> { | 
 | ||||||
|         return (await fetchLearningObjects(hruid, false, language)) as string[]; |     /** | ||||||
|  |      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). For learning objects | ||||||
|  |      * from the Dwengo API, this means passing through the HTML rendering from there. | ||||||
|  |      */ | ||||||
|  |     async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { | ||||||
|  |         const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; | ||||||
|  |         const html = await fetchWithLogging<string>( | ||||||
|  |             htmlUrl, | ||||||
|  |             `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, | ||||||
|  |             { | ||||||
|  |                 params: id | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         if (!html) { | ||||||
|  |             console.error(`⚠️ WARNING: Learning object "${id.hruid}" not found.`); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return html; | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | ||||||
|         const learningPaths = await fetchWithLogging<LearningPath[]>( |         const learningPaths = await fetchWithLogging<LearningPath[]>( | ||||||
|             apiUrl, |             apiUrl, | ||||||
|             `Learning paths for ${source}`, |             `Learning paths for ${source}`, | ||||||
|             params |             { params } | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         if (!learningPaths || learningPaths.length === 0) { |         if (!learningPaths || learningPaths.length === 0) { | ||||||
|  | @ -56,7 +56,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | ||||||
|         const searchResults = await fetchWithLogging<LearningPath[]>( |         const searchResults = await fetchWithLogging<LearningPath[]>( | ||||||
|             apiUrl, |             apiUrl, | ||||||
|             `Search learning paths with query "${query}"`, |             `Search learning paths with query "${query}"`, | ||||||
|             params |             { params } | ||||||
|         ); |         ); | ||||||
|         return searchResults ?? []; |         return searchResults ?? []; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,18 +1,27 @@ | ||||||
| import {FilteredLearningObject} from "../../interfaces/learningContent"; | import { | ||||||
|  |     FilteredLearningObject, | ||||||
|  |     LearningObjectIdentifier, | ||||||
|  |     LearningPathIdentifier | ||||||
|  | } from "../../interfaces/learning-content"; | ||||||
| 
 | 
 | ||||||
| export interface LearningObjectProvider { | export interface LearningObjectProvider { | ||||||
|     /** |     /** | ||||||
|      * Fetches a single learning object by its HRUID |      * Fetches a single learning object by its HRUID | ||||||
|      */ |      */ | ||||||
|     getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null>; |     getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null>; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Fetch full learning object data (metadata) |      * Fetch full learning object data (metadata) | ||||||
|      */ |      */ | ||||||
|     getLearningObjectsFromPath(hruid: string, language: string): Promise<FilteredLearningObject[]>; |     getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]>; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Fetch only learning object HRUIDs |      * Fetch only learning object HRUIDs | ||||||
|      */ |      */ | ||||||
|     getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]>; |     getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]>; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). | ||||||
|  |      */ | ||||||
|  |     getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null>; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,14 @@ | ||||||
| import {FilteredLearningObject} from "../../interfaces/learningContent"; | import { | ||||||
|  |     FilteredLearningObject, | ||||||
|  |     LearningObjectIdentifier, | ||||||
|  |     LearningPathIdentifier | ||||||
|  | } from "../../interfaces/learning-content"; | ||||||
| import dwengoApiLearningObjectProvider from "./dwengo-api/dwengo-api-learning-object-provider"; | import dwengoApiLearningObjectProvider from "./dwengo-api/dwengo-api-learning-object-provider"; | ||||||
|  | import {LearningObjectProvider} from "./learning-object-provider"; | ||||||
|  | 
 | ||||||
|  | function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { | ||||||
|  |     return dwengoApiLearningObjectProvider | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Service providing access to data about learning objects from the appropriate data source (database or Dwengo-api) |  * Service providing access to data about learning objects from the appropriate data source (database or Dwengo-api) | ||||||
|  | @ -8,22 +17,29 @@ const learningObjectService = { | ||||||
|     /** |     /** | ||||||
|      * Fetches a single learning object by its HRUID |      * Fetches a single learning object by its HRUID | ||||||
|      */ |      */ | ||||||
|     getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> { |     getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { | ||||||
|         return dwengoApiLearningObjectProvider.getLearningObjectById(hruid, language); |         return getProvider(id).getLearningObjectById(id); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Fetch full learning object data (metadata) |      * Fetch full learning object data (metadata) | ||||||
|      */ |      */ | ||||||
|     getLearningObjectsFromPath(hruid: string, language: string): Promise<FilteredLearningObject[]> { |     getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> { | ||||||
|         return dwengoApiLearningObjectProvider.getLearningObjectsFromPath(hruid, language); |         return getProvider(id).getLearningObjectsFromPath(id); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Fetch only learning object HRUIDs |      * Fetch only learning object HRUIDs | ||||||
|      */ |      */ | ||||||
|     getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> { |     getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> { | ||||||
|         return dwengoApiLearningObjectProvider.getLearningObjectIdsFromPath(hruid, language); |         return getProvider(id).getLearningObjectIdsFromPath(id); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). | ||||||
|  |      */ | ||||||
|  |     getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { | ||||||
|  |         return getProvider(id).getLearningObjectHTML(id); | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,17 +8,21 @@ import axios, { AxiosRequestConfig } from 'axios'; | ||||||
|  * |  * | ||||||
|  * @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 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. |  * @returns The response data if successful, or null if an error occurs. | ||||||
|  */ |  */ | ||||||
| export async function fetchWithLogging<T>( | export async function fetchWithLogging<T>( | ||||||
|     url: string, |     url: string, | ||||||
|     description: string, |     description: string, | ||||||
|     params?: Record<string, any> |     options?: { | ||||||
|  |         params?: Record<string, any>, | ||||||
|  |         query?: Record<string, any>, | ||||||
|  |         responseType?: "json" | "text", | ||||||
|  |     } | ||||||
| ): Promise<T | null> { | ): Promise<T | null> { | ||||||
|     try { |     try { | ||||||
|         const config: AxiosRequestConfig = params ? { params } : {}; |         const config: AxiosRequestConfig = options || {}; | ||||||
| 
 |  | ||||||
|         const response = await axios.get<T>(url, config); |         const response = await axios.get<T>(url, config); | ||||||
|         return response.data; |         return response.data; | ||||||
|     } catch (error: any) { |     } catch (error: any) { | ||||||
|  |  | ||||||
|  | @ -11,6 +11,8 @@ export const EnvVars: { [key: string]: EnvVar } = { | ||||||
|     DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, |     DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, | ||||||
|     DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, |     DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, | ||||||
|     DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, |     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" }, | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger