diff --git a/backend/package.json b/backend/package.json index 2b04044f..29c7ecbc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,10 +14,12 @@ "test:unit": "vitest" }, "dependencies": { - "@mikro-orm/core": "6.4.6", - "@mikro-orm/postgresql": "6.4.6", + "@mikro-orm/core": "^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/reflection": "6.4.6", "dotenv": "^16.4.7", "express": "^5.0.1", "uuid": "^11.1.0", diff --git a/backend/src/app.ts b/backend/src/app.ts index 7a25fa2b..b21bb9f1 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -3,6 +3,8 @@ import { initORM } from './orm.js'; import { EnvVars, getNumericEnvVar } from './util/envvars.js'; import themeRoutes from './routes/themes.js'; +import learningPathRoutes from './routes/learningPaths.js'; +import learningObjectRoutes from './routes/learningObjects.js'; import studentRouter from './routes/student.js'; import groupRouter from './routes/group.js'; @@ -32,6 +34,8 @@ app.use('/question', questionRouter); app.use('/login', loginRouter); app.use('/theme', themeRoutes); +app.use('/learningPath', learningPathRoutes); +app.use('/learningObject', learningObjectRoutes); async function startServer() { await initORM(); diff --git a/backend/src/config.ts b/backend/src/config.ts new file mode 100644 index 00000000..8fd8ec3f --- /dev/null +++ b/backend/src/config.ts @@ -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'; diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts new file mode 100644 index 00000000..4295326a --- /dev/null +++ b/backend/src/controllers/learningObjects.ts @@ -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 { + 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 { + 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' }); + } +} diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts new file mode 100644 index 00000000..903451be --- /dev/null +++ b/backend/src/controllers/learningPaths.ts @@ -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 { + 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' }); + } +} diff --git a/backend/src/controllers/themes.ts b/backend/src/controllers/themes.ts index 442eb982..817464ab 100644 --- a/backend/src/controllers/themes.ts +++ b/backend/src/controllers/themes.ts @@ -3,6 +3,7 @@ import path from 'path'; import yaml from 'js-yaml'; import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; +import { FALLBACK_LANG } from '../config.js'; interface Translations { curricula_page: { @@ -10,9 +11,6 @@ interface Translations { }; } -/** - * Laadt de vertalingen uit een YAML-bestand - */ function loadTranslations(language: string): Translations { try { const filePath = path.join(process.cwd(), '_i18n', `${language}.yml`); @@ -20,7 +18,7 @@ function loadTranslations(language: string): Translations { return yaml.load(yamlFile) as Translations; } catch (error) { console.error( - `Kan vertaling niet laden voor ${language}, fallback naar Nederlands` + `Cannot load translation for: ${language}, fallen back to Dutch` ); console.error(error); 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) { - const language = (req.query.language as string)?.toLowerCase() || 'nl'; + const language = + (req.query.language as string)?.toLowerCase() || FALLBACK_LANG; const translations = loadTranslations(language); const themeList = themes.map((theme) => { @@ -48,9 +44,6 @@ export function getThemes(req: Request, res: Response) { res.json(themeList); } -/** - * GET /themes/:theme → Geeft de HRUIDs terug voor een specifiek thema - */ export function getThemeByTitle(req: Request, res: Response) { const themeKey = req.params.theme; const theme = themes.find((t) => { @@ -60,6 +53,6 @@ export function getThemeByTitle(req: Request, res: Response) { if (theme) { res.json(theme.hruids); } else { - res.status(404).json({ error: 'Thema niet gevonden' }); + res.status(404).json({ error: 'Theme not found' }); } } diff --git a/backend/src/interfaces/learningPath.ts b/backend/src/interfaces/learningPath.ts new file mode 100644 index 00000000..1e2cc6ef --- /dev/null +++ b/backend/src/interfaces/learningPath.ts @@ -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; +} + +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; +} diff --git a/backend/src/routes/learningObjects.ts b/backend/src/routes/learningObjects.ts new file mode 100644 index 00000000..416602b5 --- /dev/null +++ b/backend/src/routes/learningObjects.ts @@ -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; diff --git a/backend/src/routes/learningPaths.ts b/backend/src/routes/learningPaths.ts new file mode 100644 index 00000000..ce580745 --- /dev/null +++ b/backend/src/routes/learningPaths.ts @@ -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; diff --git a/backend/src/routes/themes.ts b/backend/src/routes/themes.ts index 3185640e..388b3e38 100644 --- a/backend/src/routes/themes.ts +++ b/backend/src/routes/themes.ts @@ -3,7 +3,12 @@ import { getThemes, getThemeByTitle } from '../controllers/themes.js'; const router = express.Router(); +// Query: language +// Route to fetch list of {key, title, description, image} themes in their respective language router.get('/', getThemes); + +// Arg: theme (key) +// Route to fetch list of hruids based on theme router.get('/:theme', getThemeByTitle); export default router; diff --git a/backend/src/services/learningObjects.ts b/backend/src/services/learningObjects.ts new file mode 100644 index 00000000..d1d34ad2 --- /dev/null +++ b/backend/src/services/learningObjects.ts @@ -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 { + const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; + const metadata = await fetchWithLogging( + 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 { + 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 { + return (await fetchLearningObjects( + hruid, + true, + language + )) as FilteredLearningObject[]; +} + +/** + * Fetch only learning object HRUIDs + */ +export async function getLearningObjectIdsFromPath( + hruid: string, + language: string +): Promise { + return (await fetchLearningObjects(hruid, false, language)) as string[]; +} diff --git a/backend/src/services/learningPaths.ts b/backend/src/services/learningPaths.ts new file mode 100644 index 00000000..2a9f15a3 --- /dev/null +++ b/backend/src/services/learningPaths.ts @@ -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 { + 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( + 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 { + const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; + const params = { all: query, language }; + + const searchResults = await fetchWithLogging( + apiUrl, + `Search learning paths with query "${query}"`, + params + ); + return searchResults ?? []; +} diff --git a/backend/src/util/apiHelper.ts b/backend/src/util/apiHelper.ts new file mode 100644 index 00000000..76d166c8 --- /dev/null +++ b/backend/src/util/apiHelper.ts @@ -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( + url: string, + description: string, + params?: Record +): Promise { + try { + const config: AxiosRequestConfig = params ? { params } : {}; + + const response = await axios.get(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; + } +} diff --git a/package-lock.json b/package-lock.json index 8d47c5b5..a7bf6bb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,10 +32,13 @@ "name": "dwengo-1-backend", "version": "0.0.1", "dependencies": { - "@mikro-orm/core": "6.4.6", - "@mikro-orm/postgresql": "6.4.6", - "@mikro-orm/reflection": "6.4.6", + "@mikro-orm/core": "^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", + "dotenv": "^16.4.7", "express": "^5.0.1", "uuid": "^11.1.0", @@ -3391,9 +3394,19 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3437,6 +3450,12 @@ "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": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -3884,7 +3903,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4180,7 +4198,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4453,7 +4470,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5212,6 +5228,26 @@ "dev": true, "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": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -5233,7 +5269,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5249,7 +5284,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5259,7 +5293,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -5654,7 +5687,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5808,6 +5840,12 @@ "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6080,6 +6118,12 @@ "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": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", @@ -7010,6 +7054,47 @@ "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": { "version": "3.74.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", @@ -7331,6 +7416,15 @@ "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": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7961,6 +8055,12 @@ "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": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -8118,6 +8218,12 @@ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9091,6 +9197,30 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", @@ -10493,6 +10623,12 @@ "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": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -10818,6 +10954,15 @@ "funding": { "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": "*" + } } } }