Merge pull request #54 from SELab-2/feat/leerpad-object-routes
Feat/leerpad object routes
This commit is contained in:
		
						commit
						a0c9d8ffc1
					
				
					 14 changed files with 697 additions and 26 deletions
				
			
		|  | @ -14,10 +14,12 @@ | ||||||
|         "test:unit": "vitest" |         "test:unit": "vitest" | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@mikro-orm/core": "6.4.6", |         "@mikro-orm/core": "^6.4.6", | ||||||
|         "@mikro-orm/postgresql": "6.4.6", |         "@mikro-orm/postgresql": "^6.4.6", | ||||||
|  |         "@mikro-orm/reflection": "^6.4.6", | ||||||
|  |         "@types/js-yaml": "^4.0.9", | ||||||
|  |         "axios": "^1.8.1", | ||||||
|         "@mikro-orm/sqlite": "6.4.6", |         "@mikro-orm/sqlite": "6.4.6", | ||||||
|         "@mikro-orm/reflection": "6.4.6", |  | ||||||
|         "dotenv": "^16.4.7", |         "dotenv": "^16.4.7", | ||||||
|         "express": "^5.0.1", |         "express": "^5.0.1", | ||||||
|         "uuid": "^11.1.0", |         "uuid": "^11.1.0", | ||||||
|  |  | ||||||
|  | @ -3,6 +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 learningObjectRoutes from './routes/learningObjects.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'; | ||||||
|  | @ -32,6 +34,8 @@ app.use('/question', questionRouter); | ||||||
| app.use('/login', loginRouter); | app.use('/login', loginRouter); | ||||||
| 
 | 
 | ||||||
| app.use('/theme', themeRoutes); | app.use('/theme', themeRoutes); | ||||||
|  | app.use('/learningPath', learningPathRoutes); | ||||||
|  | app.use('/learningObject', learningObjectRoutes); | ||||||
| 
 | 
 | ||||||
| async function startServer() { | async function startServer() { | ||||||
|     await initORM(); |     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' }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -3,6 +3,7 @@ import path from 'path'; | ||||||
| import yaml from 'js-yaml'; | import yaml from 'js-yaml'; | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| import { themes } from '../data/themes.js'; | import { themes } from '../data/themes.js'; | ||||||
|  | import { FALLBACK_LANG } from '../config.js'; | ||||||
| 
 | 
 | ||||||
| interface Translations { | interface Translations { | ||||||
|     curricula_page: { |     curricula_page: { | ||||||
|  | @ -10,9 +11,6 @@ interface Translations { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** |  | ||||||
|  * Laadt de vertalingen uit een YAML-bestand |  | ||||||
|  */ |  | ||||||
| function loadTranslations(language: string): Translations { | function loadTranslations(language: string): Translations { | ||||||
|     try { |     try { | ||||||
|         const filePath = path.join(process.cwd(), '_i18n', `${language}.yml`); |         const filePath = path.join(process.cwd(), '_i18n', `${language}.yml`); | ||||||
|  | @ -20,7 +18,7 @@ function loadTranslations(language: string): Translations { | ||||||
|         return yaml.load(yamlFile) as Translations; |         return yaml.load(yamlFile) as Translations; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|         console.error( |         console.error( | ||||||
|             `Kan vertaling niet laden voor ${language}, fallback naar Nederlands` |             `Cannot load translation for: ${language}, fallen back to Dutch` | ||||||
|         ); |         ); | ||||||
|         console.error(error); |         console.error(error); | ||||||
|         const fallbackPath = path.join(process.cwd(), '_i18n', 'nl.yml'); |         const fallbackPath = path.join(process.cwd(), '_i18n', 'nl.yml'); | ||||||
|  | @ -28,11 +26,9 @@ function loadTranslations(language: string): Translations { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** |  | ||||||
|  * GET /themes → Haalt de lijst met thema's op inclusief vertalingen |  | ||||||
|  */ |  | ||||||
| export function getThemes(req: Request, res: Response) { | export function getThemes(req: Request, res: Response) { | ||||||
|     const language = (req.query.language as string)?.toLowerCase() || 'nl'; |     const language = | ||||||
|  |         (req.query.language as string)?.toLowerCase() || FALLBACK_LANG; | ||||||
|     const translations = loadTranslations(language); |     const translations = loadTranslations(language); | ||||||
| 
 | 
 | ||||||
|     const themeList = themes.map((theme) => { |     const themeList = themes.map((theme) => { | ||||||
|  | @ -48,9 +44,6 @@ export function getThemes(req: Request, res: Response) { | ||||||
|     res.json(themeList); |     res.json(themeList); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** |  | ||||||
|  * GET /themes/:theme → Geeft de HRUIDs terug voor een specifiek thema |  | ||||||
|  */ |  | ||||||
| export function getThemeByTitle(req: Request, res: Response) { | export function getThemeByTitle(req: Request, res: Response) { | ||||||
|     const themeKey = req.params.theme; |     const themeKey = req.params.theme; | ||||||
|     const theme = themes.find((t) => { |     const theme = themes.find((t) => { | ||||||
|  | @ -60,6 +53,6 @@ export function getThemeByTitle(req: Request, res: Response) { | ||||||
|     if (theme) { |     if (theme) { | ||||||
|         res.json(theme.hruids); |         res.json(theme.hruids); | ||||||
|     } else { |     } 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(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | // Query: language
 | ||||||
|  | //  Route to fetch list of {key, title, description, image} themes in their respective language
 | ||||||
| router.get('/', getThemes); | router.get('/', getThemes); | ||||||
|  | 
 | ||||||
|  | // Arg: theme (key)
 | ||||||
|  | //  Route to fetch list of hruids based on theme
 | ||||||
| router.get('/:theme', getThemeByTitle); | router.get('/:theme', getThemeByTitle); | ||||||
| 
 | 
 | ||||||
| export default router; | 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; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										167
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										167
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -32,10 +32,13 @@ | ||||||
|             "name": "dwengo-1-backend", |             "name": "dwengo-1-backend", | ||||||
|             "version": "0.0.1", |             "version": "0.0.1", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@mikro-orm/core": "6.4.6", |                 "@mikro-orm/core": "^6.4.6", | ||||||
|                 "@mikro-orm/postgresql": "6.4.6", |                 "@mikro-orm/postgresql": "^6.4.6", | ||||||
|                 "@mikro-orm/reflection": "6.4.6", |                 "@mikro-orm/reflection": "^6.4.6", | ||||||
|  |                 "@types/js-yaml": "^4.0.9", | ||||||
|  |                 "axios": "^1.8.1", | ||||||
|                 "@mikro-orm/sqlite": "6.4.6", |                 "@mikro-orm/sqlite": "6.4.6", | ||||||
|  | 
 | ||||||
|                 "dotenv": "^16.4.7", |                 "dotenv": "^16.4.7", | ||||||
|                 "express": "^5.0.1", |                 "express": "^5.0.1", | ||||||
|                 "uuid": "^11.1.0", |                 "uuid": "^11.1.0", | ||||||
|  | @ -3391,9 +3394,19 @@ | ||||||
|             "version": "0.4.0", |             "version": "0.4.0", | ||||||
|             "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", |             "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", | ||||||
|             "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", |             "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT" |             "license": "MIT" | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/axios": { | ||||||
|  |             "version": "1.8.1", | ||||||
|  |             "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", | ||||||
|  |             "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", | ||||||
|  |             "license": "MIT", | ||||||
|  |             "dependencies": { | ||||||
|  |                 "follow-redirects": "^1.15.6", | ||||||
|  |                 "form-data": "^4.0.0", | ||||||
|  |                 "proxy-from-env": "^1.1.0" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "node_modules/balanced-match": { |         "node_modules/balanced-match": { | ||||||
|             "version": "1.0.2", |             "version": "1.0.2", | ||||||
|             "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", |             "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", | ||||||
|  | @ -3437,6 +3450,12 @@ | ||||||
|                 "url": "https://github.com/sponsors/antfu" |                 "url": "https://github.com/sponsors/antfu" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/bmp-js": { | ||||||
|  |             "version": "0.1.0", | ||||||
|  |             "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", | ||||||
|  |             "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", | ||||||
|  |             "license": "MIT" | ||||||
|  |         }, | ||||||
|         "node_modules/bl": { |         "node_modules/bl": { | ||||||
|             "version": "4.1.0", |             "version": "4.1.0", | ||||||
|             "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", |             "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", | ||||||
|  | @ -3884,7 +3903,6 @@ | ||||||
|             "version": "1.0.8", |             "version": "1.0.8", | ||||||
|             "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", |             "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", | ||||||
|             "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", |             "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "delayed-stream": "~1.0.0" |                 "delayed-stream": "~1.0.0" | ||||||
|  | @ -4180,7 +4198,6 @@ | ||||||
|             "version": "1.0.0", |             "version": "1.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", |             "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", | ||||||
|             "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", |             "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">=0.4.0" |                 "node": ">=0.4.0" | ||||||
|  | @ -4453,7 +4470,6 @@ | ||||||
|             "version": "2.1.0", |             "version": "2.1.0", | ||||||
|             "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", |             "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", | ||||||
|             "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", |             "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "es-errors": "^1.3.0", |                 "es-errors": "^1.3.0", | ||||||
|  | @ -5212,6 +5228,26 @@ | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "ISC" |             "license": "ISC" | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/follow-redirects": { | ||||||
|  |             "version": "1.15.9", | ||||||
|  |             "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", | ||||||
|  |             "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", | ||||||
|  |             "funding": [ | ||||||
|  |                 { | ||||||
|  |                     "type": "individual", | ||||||
|  |                     "url": "https://github.com/sponsors/RubenVerborgh" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "license": "MIT", | ||||||
|  |             "engines": { | ||||||
|  |                 "node": ">=4.0" | ||||||
|  |             }, | ||||||
|  |             "peerDependenciesMeta": { | ||||||
|  |                 "debug": { | ||||||
|  |                     "optional": true | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "node_modules/foreground-child": { |         "node_modules/foreground-child": { | ||||||
|             "version": "3.3.0", |             "version": "3.3.0", | ||||||
|             "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", |             "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", | ||||||
|  | @ -5233,7 +5269,6 @@ | ||||||
|             "version": "4.0.2", |             "version": "4.0.2", | ||||||
|             "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", |             "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", | ||||||
|             "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", |             "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "asynckit": "^0.4.0", |                 "asynckit": "^0.4.0", | ||||||
|  | @ -5249,7 +5284,6 @@ | ||||||
|             "version": "1.52.0", |             "version": "1.52.0", | ||||||
|             "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", |             "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", | ||||||
|             "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", |             "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">= 0.6" |                 "node": ">= 0.6" | ||||||
|  | @ -5259,7 +5293,6 @@ | ||||||
|             "version": "2.1.35", |             "version": "2.1.35", | ||||||
|             "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", |             "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", | ||||||
|             "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", |             "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "mime-db": "1.52.0" |                 "mime-db": "1.52.0" | ||||||
|  | @ -5654,7 +5687,6 @@ | ||||||
|             "version": "1.0.2", |             "version": "1.0.2", | ||||||
|             "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", |             "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", | ||||||
|             "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", |             "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "has-symbols": "^1.0.3" |                 "has-symbols": "^1.0.3" | ||||||
|  | @ -5808,6 +5840,12 @@ | ||||||
|                 "node": ">=0.10.0" |                 "node": ">=0.10.0" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/idb-keyval": { | ||||||
|  |             "version": "6.2.1", | ||||||
|  |             "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", | ||||||
|  |             "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", | ||||||
|  |             "license": "Apache-2.0" | ||||||
|  |         }, | ||||||
|         "node_modules/ieee754": { |         "node_modules/ieee754": { | ||||||
|             "version": "1.2.1", |             "version": "1.2.1", | ||||||
|             "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", |             "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", | ||||||
|  | @ -6080,6 +6118,12 @@ | ||||||
|                 "url": "https://github.com/sponsors/sindresorhus" |                 "url": "https://github.com/sponsors/sindresorhus" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/is-url": { | ||||||
|  |             "version": "1.2.4", | ||||||
|  |             "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", | ||||||
|  |             "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", | ||||||
|  |             "license": "MIT" | ||||||
|  |         }, | ||||||
|         "node_modules/is-what": { |         "node_modules/is-what": { | ||||||
|             "version": "4.1.16", |             "version": "4.1.16", | ||||||
|             "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", |             "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", | ||||||
|  | @ -7010,6 +7054,47 @@ | ||||||
|                 "node": ">= 0.6" |                 "node": ">= 0.6" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/node-fetch": { | ||||||
|  |             "version": "2.7.0", | ||||||
|  |             "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", | ||||||
|  |             "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", | ||||||
|  |             "license": "MIT", | ||||||
|  |             "dependencies": { | ||||||
|  |                 "whatwg-url": "^5.0.0" | ||||||
|  |             }, | ||||||
|  |             "engines": { | ||||||
|  |                 "node": "4.x || >=6.0.0" | ||||||
|  |             }, | ||||||
|  |             "peerDependencies": { | ||||||
|  |                 "encoding": "^0.1.0" | ||||||
|  |             }, | ||||||
|  |             "peerDependenciesMeta": { | ||||||
|  |                 "encoding": { | ||||||
|  |                     "optional": true | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         "node_modules/node-fetch/node_modules/tr46": { | ||||||
|  |             "version": "0.0.3", | ||||||
|  |             "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", | ||||||
|  |             "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", | ||||||
|  |             "license": "MIT" | ||||||
|  |         }, | ||||||
|  |         "node_modules/node-fetch/node_modules/webidl-conversions": { | ||||||
|  |             "version": "3.0.1", | ||||||
|  |             "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", | ||||||
|  |             "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", | ||||||
|  |             "license": "BSD-2-Clause" | ||||||
|  |         }, | ||||||
|  |         "node_modules/node-fetch/node_modules/whatwg-url": { | ||||||
|  |             "version": "5.0.0", | ||||||
|  |             "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", | ||||||
|  |             "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", | ||||||
|  |             "license": "MIT", | ||||||
|  |             "dependencies": { | ||||||
|  |                 "tr46": "~0.0.3", | ||||||
|  |                 "webidl-conversions": "^3.0.0" | ||||||
|  |         }, | ||||||
|         "node_modules/node-abi": { |         "node_modules/node-abi": { | ||||||
|             "version": "3.74.0", |             "version": "3.74.0", | ||||||
|             "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", |             "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", | ||||||
|  | @ -7331,6 +7416,15 @@ | ||||||
|                 "url": "https://github.com/sponsors/sindresorhus" |                 "url": "https://github.com/sponsors/sindresorhus" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/opencollective-postinstall": { | ||||||
|  |             "version": "2.0.3", | ||||||
|  |             "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", | ||||||
|  |             "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", | ||||||
|  |             "license": "MIT", | ||||||
|  |             "bin": { | ||||||
|  |                 "opencollective-postinstall": "index.js" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "node_modules/optionator": { |         "node_modules/optionator": { | ||||||
|             "version": "0.9.4", |             "version": "0.9.4", | ||||||
|             "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", |             "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", | ||||||
|  | @ -7961,6 +8055,12 @@ | ||||||
|                 "node": ">= 0.10" |                 "node": ">= 0.10" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/proxy-from-env": { | ||||||
|  |             "version": "1.1.0", | ||||||
|  |             "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", | ||||||
|  |             "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", | ||||||
|  |             "license": "MIT" | ||||||
|  |         }, | ||||||
|         "node_modules/pump": { |         "node_modules/pump": { | ||||||
|             "version": "3.0.2", |             "version": "3.0.2", | ||||||
|             "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", |             "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", | ||||||
|  | @ -8118,6 +8218,12 @@ | ||||||
|             "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", |             "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", | ||||||
|             "license": "Apache-2.0" |             "license": "Apache-2.0" | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/regenerator-runtime": { | ||||||
|  |             "version": "0.13.11", | ||||||
|  |             "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", | ||||||
|  |             "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", | ||||||
|  |             "license": "MIT" | ||||||
|  |         }, | ||||||
|         "node_modules/require-directory": { |         "node_modules/require-directory": { | ||||||
|             "version": "2.1.1", |             "version": "2.1.1", | ||||||
|             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", |             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", | ||||||
|  | @ -9091,6 +9197,30 @@ | ||||||
|                 "node": ">=8.0.0" |                 "node": ">=8.0.0" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/tesseract.js": { | ||||||
|  |             "version": "6.0.0", | ||||||
|  |             "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.0.tgz", | ||||||
|  |             "integrity": "sha512-tqYCod1HwJzkeZw1l6XWx+ly2hhisGcBtak9MArhYwDAxL0NgeVhLJcUjqPxZMQtpgtVUzWcpZPryi+hnaQGVw==", | ||||||
|  |             "hasInstallScript": true, | ||||||
|  |             "license": "Apache-2.0", | ||||||
|  |             "dependencies": { | ||||||
|  |                 "bmp-js": "^0.1.0", | ||||||
|  |                 "idb-keyval": "^6.2.0", | ||||||
|  |                 "is-url": "^1.2.4", | ||||||
|  |                 "node-fetch": "^2.6.9", | ||||||
|  |                 "opencollective-postinstall": "^2.0.3", | ||||||
|  |                 "regenerator-runtime": "^0.13.3", | ||||||
|  |                 "tesseract.js-core": "^6.0.0", | ||||||
|  |                 "wasm-feature-detect": "^1.2.11", | ||||||
|  |                 "zlibjs": "^0.3.1" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         "node_modules/tesseract.js-core": { | ||||||
|  |             "version": "6.0.0", | ||||||
|  |             "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz", | ||||||
|  |             "integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==", | ||||||
|  |             "license": "Apache-2.0" | ||||||
|  |         }, | ||||||
|         "node_modules/tildify": { |         "node_modules/tildify": { | ||||||
|             "version": "2.0.0", |             "version": "2.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", |             "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", | ||||||
|  | @ -10493,6 +10623,12 @@ | ||||||
|                 "node": ">=18" |                 "node": ">=18" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/wasm-feature-detect": { | ||||||
|  |             "version": "1.8.0", | ||||||
|  |             "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", | ||||||
|  |             "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", | ||||||
|  |             "license": "Apache-2.0" | ||||||
|  |         }, | ||||||
|         "node_modules/webidl-conversions": { |         "node_modules/webidl-conversions": { | ||||||
|             "version": "7.0.0", |             "version": "7.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", |             "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", | ||||||
|  | @ -10818,6 +10954,15 @@ | ||||||
|             "funding": { |             "funding": { | ||||||
|                 "url": "https://github.com/sponsors/sindresorhus" |                 "url": "https://github.com/sponsors/sindresorhus" | ||||||
|             } |             } | ||||||
|  |         }, | ||||||
|  |         "node_modules/zlibjs": { | ||||||
|  |             "version": "0.3.1", | ||||||
|  |             "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", | ||||||
|  |             "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", | ||||||
|  |             "license": "MIT", | ||||||
|  |             "engines": { | ||||||
|  |                 "node": "*" | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl