From d7728ddd0341a9a6afeec3fbe237cdd8e7f51576 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 3 Mar 2025 22:19:09 +0100 Subject: [PATCH 01/55] refactor(backend): Preparation for learning content from multiple data sources. Refactored the service layer so that it becomes possible to add another source for learning objects and learning paths. --- backend/src/controllers/learningObjects.ts | 4 +- backend/src/controllers/learningPaths.ts | 2 +- .../{learningPath.ts => learningContent.ts} | 4 +- .../dwengo-api-learning-object-provider.ts} | 113 ++++++++++-------- .../dwengo-api-learning-path-provider.ts | 65 ++++++++++ .../learning-object-provider.ts | 18 +++ .../learning-object-service.ts | 30 +++++ .../learning-path-provider.ts | 16 +++ .../learning-content/learning-path-service.ts | 23 ++++ backend/src/services/learningPaths.ts | 61 ---------- 10 files changed, 219 insertions(+), 117 deletions(-) rename backend/src/interfaces/{learningPath.ts => learningContent.ts} (96%) rename backend/src/services/{learningObjects.ts => learning-content/dwengo-api/dwengo-api-learning-object-provider.ts} (52%) create mode 100644 backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts create mode 100644 backend/src/services/learning-content/learning-object-provider.ts create mode 100644 backend/src/services/learning-content/learning-object-service.ts create mode 100644 backend/src/services/learning-content/learning-path-provider.ts create mode 100644 backend/src/services/learning-content/learning-path-service.ts delete mode 100644 backend/src/services/learningPaths.ts diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts index 4295326a..e0a7cd4a 100644 --- a/backend/src/controllers/learningObjects.ts +++ b/backend/src/controllers/learningObjects.ts @@ -3,9 +3,9 @@ import { getLearningObjectById, getLearningObjectIdsFromPath, getLearningObjectsFromPath, -} from '../services/learningObjects.js'; +} from '../services/learning-content/dwengo-api/dwengo-api-learning-object-provider.js'; import { FALLBACK_LANG } from '../config.js'; -import { FilteredLearningObject } from '../interfaces/learningPath'; +import { FilteredLearningObject } from '../interfaces/learningContent'; export async function getAllLearningObjects( req: Request, diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts index 903451be..f05532d4 100644 --- a/backend/src/controllers/learningPaths.ts +++ b/backend/src/controllers/learningPaths.ts @@ -4,7 +4,7 @@ import { FALLBACK_LANG } from '../config.js'; import { fetchLearningPaths, searchLearningPaths, -} from '../services/learningPaths.js'; +} from '../services/learning-content/dwengo-api/dwengo-api-learning-path-provider.js'; /** * Fetch learning paths based on query parameters. */ diff --git a/backend/src/interfaces/learningPath.ts b/backend/src/interfaces/learningContent.ts similarity index 96% rename from backend/src/interfaces/learningPath.ts rename to backend/src/interfaces/learningContent.ts index 1e2cc6ef..9811543e 100644 --- a/backend/src/interfaces/learningPath.ts +++ b/backend/src/interfaces/learningContent.ts @@ -20,7 +20,7 @@ export interface LearningObjectNode { updatedAt: string; } -export interface LearningPath { +export interface LearningContent { _id: string; language: string; hruid: string; @@ -93,6 +93,6 @@ export interface FilteredLearningObject { export interface LearningPathResponse { success: boolean; source: string; - data: LearningPath[] | null; + data: LearningContent[] | null; message?: string; } diff --git a/backend/src/services/learningObjects.ts b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts similarity index 52% rename from backend/src/services/learningObjects.ts rename to backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts index d1d34ad2..c5e52695 100644 --- a/backend/src/services/learningObjects.ts +++ b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts @@ -1,13 +1,20 @@ -import { DWENGO_API_BASE } from '../config.js'; -import { fetchWithLogging } from '../util/apiHelper.js'; +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'; +} from '../../../interfaces/learningContent.js'; +import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; +import {LearningObjectProvider} from "../learning-object-provider"; +/** + * Helper function to convert the learning object metadata retrieved from the API to a FilteredLearningObject which + * our API should return. + * @param data + * @param htmlUrl + */ function filterData( data: LearningObjectMetadata, htmlUrl: string @@ -36,29 +43,7 @@ function filterData( } /** - * 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) + * Generic helper function to fetch learning objects (full data or just HRUIDs) */ async function fetchLearningObjects( hruid: string, @@ -67,7 +52,7 @@ async function fetchLearningObjects( ): Promise { try { const learningPathResponse: LearningPathResponse = - await fetchLearningPaths( + await dwengoApiLearningPathProvider.fetchLearningPaths( [hruid], language, `Learning path for HRUID "${hruid}"` @@ -93,7 +78,7 @@ async function fetchLearningObjects( return await Promise.all( nodes.map(async (node) => { - return getLearningObjectById( + return dwengoApiLearningObjectProvider.getLearningObjectById( node.learningobject_hruid, language ); @@ -109,26 +94,52 @@ async function fetchLearningObjects( } } -/** - * Fetch full learning object data (metadata) - */ -export async function getLearningObjectsFromPath( - hruid: string, - language: string -): Promise { - return (await fetchLearningObjects( - hruid, - true, - language - )) as FilteredLearningObject[]; -} +const dwengoApiLearningObjectProvider: LearningObjectProvider = { + /** + * Fetches a single learning object by its HRUID + */ + async 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})` + ); -/** - * Fetch only learning object HRUIDs - */ -export async function getLearningObjectIdsFromPath( - hruid: string, - language: string -): Promise { - return (await fetchLearningObjects(hruid, false, language)) as string[]; -} + 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); + }, + + /** + * Fetch full learning object data (metadata) + */ + async getLearningObjectsFromPath( + hruid: string, + language: string + ): Promise { + return (await fetchLearningObjects( + hruid, + true, + language + )) as FilteredLearningObject[]; + }, + + /** + * Fetch only learning object HRUIDs + */ + async getLearningObjectIdsFromPath( + hruid: string, + language: string + ): Promise { + return (await fetchLearningObjects(hruid, false, language)) as string[]; + } +}; + +export default dwengoApiLearningObjectProvider; diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts new file mode 100644 index 00000000..23662646 --- /dev/null +++ b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts @@ -0,0 +1,65 @@ +import { fetchWithLogging } from '../../../util/apiHelper.js'; +import { DWENGO_API_BASE } from '../../../config.js'; +import { + LearningContent, + LearningPathResponse, +} from '../../../interfaces/learningContent.js'; +import {LearningPathProvider} from "../learning-path-provider"; + +const dwengoApiLearningPathProvider: LearningPathProvider = { + async 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, + }; + }, + async 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 ?? []; + } +}; + +export default dwengoApiLearningPathProvider; diff --git a/backend/src/services/learning-content/learning-object-provider.ts b/backend/src/services/learning-content/learning-object-provider.ts new file mode 100644 index 00000000..e37c4442 --- /dev/null +++ b/backend/src/services/learning-content/learning-object-provider.ts @@ -0,0 +1,18 @@ +import {FilteredLearningObject} from "../../interfaces/learningContent"; + +export interface LearningObjectProvider { + /** + * Fetches a single learning object by its HRUID + */ + getLearningObjectById(hruid: string, language: string): Promise; + + /** + * Fetch full learning object data (metadata) + */ + getLearningObjectsFromPath(hruid: string, language: string): Promise; + + /** + * Fetch only learning object HRUIDs + */ + getLearningObjectIdsFromPath(hruid: string, language: string): Promise; +} diff --git a/backend/src/services/learning-content/learning-object-service.ts b/backend/src/services/learning-content/learning-object-service.ts new file mode 100644 index 00000000..e0beec60 --- /dev/null +++ b/backend/src/services/learning-content/learning-object-service.ts @@ -0,0 +1,30 @@ +import {FilteredLearningObject} from "../../interfaces/learningContent"; +import dwengoApiLearningObjectProvider from "./dwengo-api/dwengo-api-learning-object-provider"; + +/** + * Service providing access to data about learning objects from the appropriate data source (database or Dwengo-api) + */ +const learningObjectService = { + /** + * Fetches a single learning object by its HRUID + */ + getLearningObjectById(hruid: string, language: string): Promise { + return dwengoApiLearningObjectProvider.getLearningObjectById(hruid, language); + }, + + /** + * Fetch full learning object data (metadata) + */ + getLearningObjectsFromPath(hruid: string, language: string): Promise { + return dwengoApiLearningObjectProvider.getLearningObjectsFromPath(hruid, language); + }, + + /** + * Fetch only learning object HRUIDs + */ + getLearningObjectIdsFromPath(hruid: string, language: string): Promise { + return dwengoApiLearningObjectProvider.getLearningObjectIdsFromPath(hruid, language); + } +}; + +export default learningObjectService; diff --git a/backend/src/services/learning-content/learning-path-provider.ts b/backend/src/services/learning-content/learning-path-provider.ts new file mode 100644 index 00000000..f236cd82 --- /dev/null +++ b/backend/src/services/learning-content/learning-path-provider.ts @@ -0,0 +1,16 @@ +import {LearningContent, LearningPathResponse} from "../../interfaces/learningContent"; + +/** + * Generic interface for a service which provides access to learning paths from a data source. + */ +export interface LearningPathProvider { + /** + * Fetch the learning paths with the given hruids from the data source. + */ + fetchLearningPaths(hruids: string[], language: string, source: string): Promise; + + /** + * Search learning paths in the data source using the given search string. + */ + searchLearningPaths(query: string, language: string): Promise; +} diff --git a/backend/src/services/learning-content/learning-path-service.ts b/backend/src/services/learning-content/learning-path-service.ts new file mode 100644 index 00000000..31e10558 --- /dev/null +++ b/backend/src/services/learning-content/learning-path-service.ts @@ -0,0 +1,23 @@ +import {LearningContent, LearningPathResponse} from "../../interfaces/learningContent"; +import dwengoApiLearningPathProvider from "./dwengo-api/dwengo-api-learning-path-provider"; + +/** + * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) + */ +const learningPathService = { + /** + * Fetch the learning paths with the given hruids from the data source. + */ + fetchLearningPaths(hruids: string[], language: string, source: string): Promise { + return dwengoApiLearningPathProvider.fetchLearningPaths(hruids, language, source); + }, + + /** + * Search learning paths in the data source using the given search string. + */ + searchLearningPaths(query: string, language: string): Promise { + return dwengoApiLearningPathProvider.searchLearningPaths(query, language); + } +} + +export default learningPathService; diff --git a/backend/src/services/learningPaths.ts b/backend/src/services/learningPaths.ts deleted file mode 100644 index 2a9f15a3..00000000 --- a/backend/src/services/learningPaths.ts +++ /dev/null @@ -1,61 +0,0 @@ -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 ?? []; -} From e660c185b53d1cb9883c6f089bc632fb6d739b2f Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 3 Mar 2025 22:21:47 +0100 Subject: [PATCH 02/55] fix(backend): Forgot to adjust some files. --- backend/src/controllers/learningObjects.ts | 12 ++++-------- backend/src/controllers/learningPaths.ts | 9 +++------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts index e0a7cd4a..2455b49c 100644 --- a/backend/src/controllers/learningObjects.ts +++ b/backend/src/controllers/learningObjects.ts @@ -1,11 +1,7 @@ import { Request, Response } from 'express'; -import { - getLearningObjectById, - getLearningObjectIdsFromPath, - getLearningObjectsFromPath, -} from '../services/learning-content/dwengo-api/dwengo-api-learning-object-provider.js'; 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, @@ -23,9 +19,9 @@ export async function getAllLearningObjects( let learningObjects: FilteredLearningObject[] | string[]; if (full) { - learningObjects = await getLearningObjectsFromPath(hruid, language); + learningObjects = await learningObjectService.getLearningObjectsFromPath(hruid, language); } else { - learningObjects = await getLearningObjectIdsFromPath( + learningObjects = await learningObjectService.getLearningObjectIdsFromPath( hruid, language ); @@ -51,7 +47,7 @@ export async function getLearningObject( return; } - const learningObject = await getLearningObjectById(hruid, language); + const learningObject = await learningObjectService.getLearningObjectById(hruid, language); res.json(learningObject); } catch (error) { console.error('Error fetching learning object:', error); diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts index f05532d4..af521157 100644 --- a/backend/src/controllers/learningPaths.ts +++ b/backend/src/controllers/learningPaths.ts @@ -1,10 +1,7 @@ import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; import { FALLBACK_LANG } from '../config.js'; -import { - fetchLearningPaths, - searchLearningPaths, -} from '../services/learning-content/dwengo-api/dwengo-api-learning-path-provider.js'; +import learningPathService from "../services/learning-content/learning-path-service"; /** * Fetch learning paths based on query parameters. */ @@ -37,7 +34,7 @@ export async function getLearningPaths( return; } } else if (searchQuery) { - const searchResults = await searchLearningPaths( + const searchResults = await learningPathService.searchLearningPaths( searchQuery, language ); @@ -49,7 +46,7 @@ export async function getLearningPaths( }); } - const learningPaths = await fetchLearningPaths( + const learningPaths = await learningPathService.fetchLearningPaths( hruidList, language, `HRUIDs: ${hruidList.join(', ')}` From 770c5c987931a19a04e61a7216ed89700d56161c Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 3 Mar 2025 22:25:00 +0100 Subject: [PATCH 03/55] fix(backend): Undid accidental rename of LearningPath to LearningContent. --- .../dwengo-api/dwengo-api-learning-path-provider.ts | 10 +++++----- .../learning-content/learning-path-provider.ts | 4 ++-- .../services/learning-content/learning-path-service.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts index 23662646..f7b4ff7d 100644 --- a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts @@ -1,9 +1,9 @@ import { fetchWithLogging } from '../../../util/apiHelper.js'; import { DWENGO_API_BASE } from '../../../config.js'; import { - LearningContent, + LearningPath, LearningPathResponse, -} from '../../../interfaces/learningContent.js'; +} from '../../../interfaces/learning-content.js'; import {LearningPathProvider} from "../learning-path-provider"; const dwengoApiLearningPathProvider: LearningPathProvider = { @@ -24,7 +24,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; const params = { pathIdList: JSON.stringify({ hruids }), language }; - const learningPaths = await fetchWithLogging( + const learningPaths = await fetchWithLogging( apiUrl, `Learning paths for ${source}`, params @@ -49,11 +49,11 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { async searchLearningPaths( query: string, language: string - ): Promise { + ): Promise { const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; const params = { all: query, language }; - const searchResults = await fetchWithLogging( + const searchResults = await fetchWithLogging( apiUrl, `Search learning paths with query "${query}"`, params diff --git a/backend/src/services/learning-content/learning-path-provider.ts b/backend/src/services/learning-content/learning-path-provider.ts index f236cd82..e9948963 100644 --- a/backend/src/services/learning-content/learning-path-provider.ts +++ b/backend/src/services/learning-content/learning-path-provider.ts @@ -1,4 +1,4 @@ -import {LearningContent, LearningPathResponse} from "../../interfaces/learningContent"; +import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; /** * Generic interface for a service which provides access to learning paths from a data source. @@ -12,5 +12,5 @@ export interface LearningPathProvider { /** * Search learning paths in the data source using the given search string. */ - searchLearningPaths(query: string, language: string): Promise; + searchLearningPaths(query: string, language: string): Promise; } diff --git a/backend/src/services/learning-content/learning-path-service.ts b/backend/src/services/learning-content/learning-path-service.ts index 31e10558..0bb925bf 100644 --- a/backend/src/services/learning-content/learning-path-service.ts +++ b/backend/src/services/learning-content/learning-path-service.ts @@ -1,4 +1,4 @@ -import {LearningContent, LearningPathResponse} from "../../interfaces/learningContent"; +import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; import dwengoApiLearningPathProvider from "./dwengo-api/dwengo-api-learning-path-provider"; /** @@ -15,7 +15,7 @@ const learningPathService = { /** * Search learning paths in the data source using the given search string. */ - searchLearningPaths(query: string, language: string): Promise { + searchLearningPaths(query: string, language: string): Promise { return dwengoApiLearningPathProvider.searchLearningPaths(query, language); } } From 18ee991ce3cb6d3589d8c69f69d4acba61a98b03 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 4 Mar 2025 22:35:05 +0100 Subject: [PATCH 04/55] feat(backend): Added endpoint to fetch HTML version of learning object (from Dwengo backend) Also refactored a bit to make this easier. --- backend/src/app.ts | 4 +- backend/src/config.ts | 6 +- backend/src/controllers/learning-objects.ts | 62 +++++++++++++ backend/src/controllers/learning-paths.ts | 53 +++++++++++ backend/src/controllers/learningObjects.ts | 56 ------------ backend/src/controllers/learningPaths.ts | 59 ------------- backend/src/exceptions.ts | 21 +++++ ...learningContent.ts => learning-content.ts} | 17 +++- ...learningObjects.ts => learning-objects.ts} | 10 ++- .../{learningPaths.ts => learning-paths.ts} | 2 +- .../dwengo-api-learning-object-provider.ts | 87 +++++++++++-------- .../dwengo-api-learning-path-provider.ts | 4 +- .../learning-object-provider.ts | 17 +++- .../learning-object-service.ts | 30 +++++-- backend/src/util/apiHelper.ts | 12 ++- backend/src/util/envvars.ts | 2 + 16 files changed, 264 insertions(+), 178 deletions(-) create mode 100644 backend/src/controllers/learning-objects.ts create mode 100644 backend/src/controllers/learning-paths.ts delete mode 100644 backend/src/controllers/learningObjects.ts delete mode 100644 backend/src/controllers/learningPaths.ts create mode 100644 backend/src/exceptions.ts rename backend/src/interfaces/{learningContent.ts => learning-content.ts} (86%) rename backend/src/routes/{learningObjects.ts => learning-objects.ts} (68%) rename backend/src/routes/{learningPaths.ts => learning-paths.ts} (91%) diff --git a/backend/src/app.ts b/backend/src/app.ts index b21bb9f1..123075ea 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -3,8 +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 learningPathRoutes from './routes/learning-paths.js'; +import learningObjectRoutes from './routes/learning-objects.js'; import studentRouter from './routes/student.js'; import groupRouter from './routes/group.js'; diff --git a/backend/src/config.ts b/backend/src/config.ts index 8fd8ec3f..2b9e67e3 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -5,6 +5,8 @@ // Load .env file // 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); diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts new file mode 100644 index 00000000..18bf5f26 --- /dev/null +++ b/backend/src/controllers/learning-objects.ts @@ -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 { + 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 { + const learningObjectId = getLearningObjectIdentifierFromRequest(req); + + const learningObject = await learningObjectService.getLearningObjectById(learningObjectId); + res.json(learningObject); +} + +export async function getLearningObjectHTML(req: Request, res: Response): Promise { + const learningObjectId = getLearningObjectIdentifierFromRequest(req); + + const learningObject = await learningObjectService.getLearningObjectHTML(learningObjectId); + res.send(learningObject); +} diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts new file mode 100644 index 00000000..593fd695 --- /dev/null +++ b/backend/src/controllers/learning-paths.ts @@ -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 { + 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); +} diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts deleted file mode 100644 index 2455b49c..00000000 --- a/backend/src/controllers/learningObjects.ts +++ /dev/null @@ -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 { - 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 { - 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' }); - } -} diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts deleted file mode 100644 index af521157..00000000 --- a/backend/src/controllers/learningPaths.ts +++ /dev/null @@ -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 { - 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' }); - } -} diff --git a/backend/src/exceptions.ts b/backend/src/exceptions.ts new file mode 100644 index 00000000..993a13ba --- /dev/null +++ b/backend/src/exceptions.ts @@ -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); + } +} diff --git a/backend/src/interfaces/learningContent.ts b/backend/src/interfaces/learning-content.ts similarity index 86% rename from backend/src/interfaces/learningContent.ts rename to backend/src/interfaces/learning-content.ts index 9811543e..7fd3a55d 100644 --- a/backend/src/interfaces/learningContent.ts +++ b/backend/src/interfaces/learning-content.ts @@ -1,3 +1,5 @@ +import {Language} from "../entities/content/language"; + export interface Transition { default: boolean; _id: string; @@ -9,6 +11,12 @@ export interface Transition { }; } +export interface LearningObjectIdentifier { + hruid: string; + language: Language; + version?: string; +} + export interface LearningObjectNode { _id: string; learningobject_hruid: string; @@ -20,7 +28,7 @@ export interface LearningObjectNode { updatedAt: string; } -export interface LearningContent { +export interface LearningPath { _id: string; language: string; hruid: string; @@ -37,6 +45,11 @@ export interface LearningContent { __order: number; } +export interface LearningPathIdentifier { + hruid: string; + language: Language; +} + export interface EducationalGoal { source: string; id: string; @@ -93,6 +106,6 @@ export interface FilteredLearningObject { export interface LearningPathResponse { success: boolean; source: string; - data: LearningContent[] | null; + data: LearningPath[] | null; message?: string; } diff --git a/backend/src/routes/learningObjects.ts b/backend/src/routes/learning-objects.ts similarity index 68% rename from backend/src/routes/learningObjects.ts rename to backend/src/routes/learning-objects.ts index 416602b5..b44a6b8f 100644 --- a/backend/src/routes/learningObjects.ts +++ b/backend/src/routes/learning-objects.ts @@ -1,8 +1,8 @@ import express from 'express'; import { getAllLearningObjects, - getLearningObject, -} from '../controllers/learningObjects.js'; + getLearningObject, getLearningObjectHTML, +} from '../controllers/learning-objects.js'; const router = express.Router(); @@ -24,4 +24,10 @@ router.get('/', getAllLearningObjects); // Example: http://localhost:3000/learningObject/un_ai7 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; diff --git a/backend/src/routes/learningPaths.ts b/backend/src/routes/learning-paths.ts similarity index 91% rename from backend/src/routes/learningPaths.ts rename to backend/src/routes/learning-paths.ts index ce580745..efe17312 100644 --- a/backend/src/routes/learningPaths.ts +++ b/backend/src/routes/learning-paths.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { getLearningPaths } from '../controllers/learningPaths.js'; +import { getLearningPaths } from '../controllers/learning-paths.js'; const router = express.Router(); diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts index c5e52695..981e073b 100644 --- a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts +++ b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts @@ -1,11 +1,11 @@ import { DWENGO_API_BASE } from '../../../config.js'; import { fetchWithLogging } from '../../../util/apiHelper.js'; import { - FilteredLearningObject, + FilteredLearningObject, LearningObjectIdentifier, LearningObjectMetadata, - LearningObjectNode, + LearningObjectNode, LearningPathIdentifier, LearningPathResponse, -} from '../../../interfaces/learningContent.js'; +} from '../../../interfaces/learning-content.js'; import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; 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 * our API should return. * @param data - * @param htmlUrl */ function filterData( - data: LearningObjectMetadata, - htmlUrl: string + data: LearningObjectMetadata ): FilteredLearningObject { return { key: data.hruid, // Hruid learningObject (not path) @@ -25,7 +23,7 @@ function filterData( uuid: data.uuid, version: data.version, 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, difficulty: data.difficulty, 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( - hruid: string, - full: boolean, - language: string + learningPathId: LearningPathIdentifier, + full: boolean ): Promise { try { const learningPathResponse: LearningPathResponse = await dwengoApiLearningPathProvider.fetchLearningPaths( - [hruid], - language, - `Learning path for HRUID "${hruid}"` + [learningPathId.hruid], + learningPathId.language, + `Learning path for HRUID "${learningPathId.hruid}"` ); if ( @@ -63,7 +60,7 @@ async function fetchLearningObjects( !learningPathResponse.data?.length ) { console.error( - `⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.` + `⚠️ WARNING: Learning path "${learningPathId.hruid}" exists but contains no learning objects.` ); return []; } @@ -78,10 +75,10 @@ async function fetchLearningObjects( return await Promise.all( nodes.map(async (node) => { - return dwengoApiLearningObjectProvider.getLearningObjectById( - node.learningobject_hruid, - language - ); + return dwengoApiLearningObjectProvider.getLearningObjectById({ + hruid: node.learningobject_hruid, + language: learningPathId.language + }); }) ).then((objects) => { return objects.filter((obj): obj is FilteredLearningObject => { @@ -99,46 +96,62 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { * Fetches a single learning object by its HRUID */ async getLearningObjectById( - hruid: string, - language: string + id: LearningObjectIdentifier ): Promise { - const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; + let metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; const metadata = await fetchWithLogging( metadataUrl, - `Metadata for Learning Object HRUID "${hruid}" (language ${language})` + `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, + { + params: id + } ); if (!metadata) { - console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); + console.error(`⚠️ WARNING: Learning object "${id.hruid}" not found.`); return null; } - const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`; - return filterData(metadata, htmlUrl); + return filterData(metadata); }, /** * Fetch full learning object data (metadata) */ - async getLearningObjectsFromPath( - hruid: string, - language: string - ): Promise { + async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { return (await fetchLearningObjects( - hruid, + id, true, - language )) as FilteredLearningObject[]; }, /** * Fetch only learning object HRUIDs */ - async getLearningObjectIdsFromPath( - hruid: string, - language: string - ): Promise { - return (await fetchLearningObjects(hruid, false, language)) as string[]; + async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { + return (await fetchLearningObjects(id, false)) 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 { + const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; + const html = await fetchWithLogging( + 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; } }; diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts index f7b4ff7d..5811398c 100644 --- a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts @@ -27,7 +27,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { const learningPaths = await fetchWithLogging( apiUrl, `Learning paths for ${source}`, - params + { params } ); if (!learningPaths || learningPaths.length === 0) { @@ -56,7 +56,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { const searchResults = await fetchWithLogging( apiUrl, `Search learning paths with query "${query}"`, - params + { params } ); return searchResults ?? []; } diff --git a/backend/src/services/learning-content/learning-object-provider.ts b/backend/src/services/learning-content/learning-object-provider.ts index e37c4442..70190a1a 100644 --- a/backend/src/services/learning-content/learning-object-provider.ts +++ b/backend/src/services/learning-content/learning-object-provider.ts @@ -1,18 +1,27 @@ -import {FilteredLearningObject} from "../../interfaces/learningContent"; +import { + FilteredLearningObject, + LearningObjectIdentifier, + LearningPathIdentifier +} from "../../interfaces/learning-content"; export interface LearningObjectProvider { /** * Fetches a single learning object by its HRUID */ - getLearningObjectById(hruid: string, language: string): Promise; + getLearningObjectById(id: LearningObjectIdentifier): Promise; /** * Fetch full learning object data (metadata) */ - getLearningObjectsFromPath(hruid: string, language: string): Promise; + getLearningObjectsFromPath(id: LearningPathIdentifier): Promise; /** * Fetch only learning object HRUIDs */ - getLearningObjectIdsFromPath(hruid: string, language: string): Promise; + getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise; + + /** + * Obtain a HTML-rendering of the learning object with the given identifier (as a string). + */ + getLearningObjectHTML(id: LearningObjectIdentifier): Promise; } diff --git a/backend/src/services/learning-content/learning-object-service.ts b/backend/src/services/learning-content/learning-object-service.ts index e0beec60..4daedb79 100644 --- a/backend/src/services/learning-content/learning-object-service.ts +++ b/backend/src/services/learning-content/learning-object-service.ts @@ -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 {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) @@ -8,22 +17,29 @@ const learningObjectService = { /** * Fetches a single learning object by its HRUID */ - getLearningObjectById(hruid: string, language: string): Promise { - return dwengoApiLearningObjectProvider.getLearningObjectById(hruid, language); + getLearningObjectById(id: LearningObjectIdentifier): Promise { + return getProvider(id).getLearningObjectById(id); }, /** * Fetch full learning object data (metadata) */ - getLearningObjectsFromPath(hruid: string, language: string): Promise { - return dwengoApiLearningObjectProvider.getLearningObjectsFromPath(hruid, language); + getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { + return getProvider(id).getLearningObjectsFromPath(id); }, /** * Fetch only learning object HRUIDs */ - getLearningObjectIdsFromPath(hruid: string, language: string): Promise { - return dwengoApiLearningObjectProvider.getLearningObjectIdsFromPath(hruid, language); + getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { + return getProvider(id).getLearningObjectIdsFromPath(id); + }, + + /** + * Obtain a HTML-rendering of the learning object with the given identifier (as a string). + */ + getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + return getProvider(id).getLearningObjectHTML(id); } }; diff --git a/backend/src/util/apiHelper.ts b/backend/src/util/apiHelper.ts index 76d166c8..8733a7a4 100644 --- a/backend/src/util/apiHelper.ts +++ b/backend/src/util/apiHelper.ts @@ -8,17 +8,21 @@ import axios, { AxiosRequestConfig } from 'axios'; * * @param url The API endpoint to fetch from. * @param description A short description of what is being fetched (for logging). - * @param params + * @param options Contains further options such as params (the query params) and responseType (whether the response + * should be parsed as JSON ("json") or whether it should be returned as plain text ("text") * @returns The response data if successful, or null if an error occurs. */ export async function fetchWithLogging( url: string, description: string, - params?: Record + options?: { + params?: Record, + query?: Record, + responseType?: "json" | "text", + } ): Promise { try { - const config: AxiosRequestConfig = params ? { params } : {}; - + const config: AxiosRequestConfig = options || {}; const response = await axios.get(url, config); return response.data; } catch (error: any) { diff --git a/backend/src/util/envvars.ts b/backend/src/util/envvars.ts index 5a06ac22..8cb82487 100644 --- a/backend/src/util/envvars.ts +++ b/backend/src/util/envvars.ts @@ -11,6 +11,8 @@ export const EnvVars: { [key: string]: EnvVar } = { DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, + LearningContentRepoApiBaseUrl: { key: PREFIX + "LEARNING_CONTENT_REPO_API_BASE_URL", defaultValue: "https://dwengo.org/backend/api"}, + FallbackLanguage: { key: PREFIX + "FALLBACK_LANGUAGE", defaultValue: "nl" }, } as const; /** From cc6947dd3c971e4f6a568a10278ff81e77ade3b0 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 4 Mar 2025 22:38:21 +0100 Subject: [PATCH 05/55] refactor(backend): Changed folder structure for learning content services. --- backend/src/controllers/learning-objects.ts | 2 +- backend/src/controllers/learning-paths.ts | 2 +- .../dwengo-api-learning-object-provider.ts | 10 +++++----- .../learning-object-provider.ts | 0 .../learning-object-service.ts | 2 +- .../dwengo-api-learning-path-provider.ts | 8 ++++---- .../learning-path-provider.ts | 0 .../learning-path-service.ts | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) rename backend/src/services/{learning-content/dwengo-api => learning-objects}/dwengo-api-learning-object-provider.ts (94%) rename backend/src/services/{learning-content => learning-objects}/learning-object-provider.ts (100%) rename backend/src/services/{learning-content => learning-objects}/learning-object-service.ts (93%) rename backend/src/services/{learning-content/dwengo-api => learning-paths}/dwengo-api-learning-path-provider.ts (88%) rename backend/src/services/{learning-content => learning-paths}/learning-path-provider.ts (100%) rename backend/src/services/{learning-content => learning-paths}/learning-path-service.ts (90%) diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index 18bf5f26..c4dd8d08 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -1,7 +1,7 @@ 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 learningObjectService from "../services/learning-objects/learning-object-service"; import {EnvVars, getEnvVar} from "../util/envvars"; import {Language} from "../entities/content/language"; import {BadRequestException} from "../exceptions"; diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts index 593fd695..cb5ae07e 100644 --- a/backend/src/controllers/learning-paths.ts +++ b/backend/src/controllers/learning-paths.ts @@ -1,7 +1,7 @@ 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 learningPathService from "../services/learning-paths/learning-path-service"; import {NotFoundException} from "../exceptions"; /** diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts similarity index 94% rename from backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts rename to backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts index 981e073b..bef08dfe 100644 --- a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-object-provider.ts +++ b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts @@ -1,13 +1,13 @@ -import { DWENGO_API_BASE } from '../../../config.js'; -import { fetchWithLogging } from '../../../util/apiHelper.js'; +import { DWENGO_API_BASE } from '../../config.js'; +import { fetchWithLogging } from '../../util/apiHelper.js'; import { FilteredLearningObject, LearningObjectIdentifier, LearningObjectMetadata, LearningObjectNode, LearningPathIdentifier, LearningPathResponse, -} from '../../../interfaces/learning-content.js'; -import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; -import {LearningObjectProvider} from "../learning-object-provider"; +} from '../../interfaces/learning-content.js'; +import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; +import {LearningObjectProvider} from "./learning-object-provider"; /** * Helper function to convert the learning object metadata retrieved from the API to a FilteredLearningObject which diff --git a/backend/src/services/learning-content/learning-object-provider.ts b/backend/src/services/learning-objects/learning-object-provider.ts similarity index 100% rename from backend/src/services/learning-content/learning-object-provider.ts rename to backend/src/services/learning-objects/learning-object-provider.ts diff --git a/backend/src/services/learning-content/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts similarity index 93% rename from backend/src/services/learning-content/learning-object-service.ts rename to backend/src/services/learning-objects/learning-object-service.ts index 4daedb79..587e8968 100644 --- a/backend/src/services/learning-content/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -3,7 +3,7 @@ import { LearningObjectIdentifier, LearningPathIdentifier } from "../../interfaces/learning-content"; -import dwengoApiLearningObjectProvider from "./dwengo-api/dwengo-api-learning-object-provider"; +import dwengoApiLearningObjectProvider from "./dwengo-api-learning-object-provider"; import {LearningObjectProvider} from "./learning-object-provider"; function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { diff --git a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts similarity index 88% rename from backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts rename to backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts index 5811398c..ae6dff0a 100644 --- a/backend/src/services/learning-content/dwengo-api/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts @@ -1,10 +1,10 @@ -import { fetchWithLogging } from '../../../util/apiHelper.js'; -import { DWENGO_API_BASE } from '../../../config.js'; +import { fetchWithLogging } from '../../util/apiHelper.js'; +import { DWENGO_API_BASE } from '../../config.js'; import { LearningPath, LearningPathResponse, -} from '../../../interfaces/learning-content.js'; -import {LearningPathProvider} from "../learning-path-provider"; +} from '../../interfaces/learning-content.js'; +import {LearningPathProvider} from "./learning-path-provider"; const dwengoApiLearningPathProvider: LearningPathProvider = { async fetchLearningPaths( diff --git a/backend/src/services/learning-content/learning-path-provider.ts b/backend/src/services/learning-paths/learning-path-provider.ts similarity index 100% rename from backend/src/services/learning-content/learning-path-provider.ts rename to backend/src/services/learning-paths/learning-path-provider.ts diff --git a/backend/src/services/learning-content/learning-path-service.ts b/backend/src/services/learning-paths/learning-path-service.ts similarity index 90% rename from backend/src/services/learning-content/learning-path-service.ts rename to backend/src/services/learning-paths/learning-path-service.ts index 0bb925bf..fd37cd38 100644 --- a/backend/src/services/learning-content/learning-path-service.ts +++ b/backend/src/services/learning-paths/learning-path-service.ts @@ -1,5 +1,5 @@ import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; -import dwengoApiLearningPathProvider from "./dwengo-api/dwengo-api-learning-path-provider"; +import dwengoApiLearningPathProvider from "./dwengo-api-learning-path-provider"; /** * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) From 2d9f17484c54b14798358df29cd67dd835154afc Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 4 Mar 2025 23:33:03 +0100 Subject: [PATCH 06/55] feat(backend): Added skeleton for a database learning object/path provider. --- .../database-learning-object-provider.ts | 42 +++++++++++++++++++ .../learning-object-service.ts | 8 +++- .../database-learning-path-provider.ts | 23 ++++++++++ .../learning-paths/learning-path-service.ts | 35 +++++++++++++--- backend/src/util/envvars.ts | 1 + 5 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 backend/src/services/learning-objects/database-learning-object-provider.ts create mode 100644 backend/src/services/learning-paths/database-learning-path-provider.ts diff --git a/backend/src/services/learning-objects/database-learning-object-provider.ts b/backend/src/services/learning-objects/database-learning-object-provider.ts new file mode 100644 index 00000000..85b97614 --- /dev/null +++ b/backend/src/services/learning-objects/database-learning-object-provider.ts @@ -0,0 +1,42 @@ +import {LearningObjectProvider} from "./learning-object-provider"; +import { + FilteredLearningObject, + LearningObjectIdentifier, + LearningPathIdentifier +} from "../../interfaces/learning-content"; + +/** + * Service providing access to data about learning objects from the database + */ +const databaseLearningObjectProvider: LearningObjectProvider = { + /** + * Fetches a single learning object by its HRUID + */ + getLearningObjectById(id: LearningObjectIdentifier): Promise { + return Promise.resolve(null); // TODO + }, + + /** + * Fetch full learning object data (metadata) + */ + getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + return Promise.resolve(null); // TODO + }, + + /** + * Fetch only learning object HRUIDs + */ + getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { + return Promise.resolve([]);// TODO + }, + + /** + * Obtain a HTML-rendering of the learning object with the given identifier (as a string). + */ + getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { + return Promise.resolve([]); // TODO + } + +} + +export default databaseLearningObjectProvider; diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 587e8968..51152ec8 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -5,9 +5,15 @@ import { } from "../../interfaces/learning-content"; import dwengoApiLearningObjectProvider from "./dwengo-api-learning-object-provider"; import {LearningObjectProvider} from "./learning-object-provider"; +import {EnvVars, getEnvVar} from "../../util/envvars"; +import databaseLearningObjectProvider from "./database-learning-object-provider"; function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { - return dwengoApiLearningObjectProvider + if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) { + return databaseLearningObjectProvider; + } else { + return dwengoApiLearningObjectProvider; + } } /** diff --git a/backend/src/services/learning-paths/database-learning-path-provider.ts b/backend/src/services/learning-paths/database-learning-path-provider.ts new file mode 100644 index 00000000..292bb79e --- /dev/null +++ b/backend/src/services/learning-paths/database-learning-path-provider.ts @@ -0,0 +1,23 @@ +import {LearningPathProvider} from "./learning-path-provider"; +import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; + +/** + * Service providing access to data about learning paths from the database. + */ +const databaseLearningPathProvider: LearningPathProvider = { + /** + * Fetch the learning paths with the given hruids from the database. + */ + fetchLearningPaths(hruids: string[], language: string, source: string): Promise { + throw new Error("Not yet implemented"); // TODO + }, + + /** + * Search learning paths in the database using the given search string. + */ + searchLearningPaths(query: string, language: string): Promise { + return Promise.resolve([]); // TODO + } +} + +export default databaseLearningPathProvider; diff --git a/backend/src/services/learning-paths/learning-path-service.ts b/backend/src/services/learning-paths/learning-path-service.ts index fd37cd38..a2776554 100644 --- a/backend/src/services/learning-paths/learning-path-service.ts +++ b/backend/src/services/learning-paths/learning-path-service.ts @@ -1,5 +1,13 @@ -import {LearningPath, LearningPathResponse} from "../../interfaces/learning-content"; +import { + LearningPath, + LearningPathResponse +} from "../../interfaces/learning-content"; import dwengoApiLearningPathProvider from "./dwengo-api-learning-path-provider"; +import databaseLearningPathProvider from "./database-learning-path-provider"; +import {EnvVars, getEnvVar} from "../../util/envvars"; + +const userContentPrefix = getEnvVar(EnvVars.UserContentPrefix); +const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider] /** * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) @@ -8,15 +16,32 @@ const learningPathService = { /** * Fetch the learning paths with the given hruids from the data source. */ - fetchLearningPaths(hruids: string[], language: string, source: string): Promise { - return dwengoApiLearningPathProvider.fetchLearningPaths(hruids, language, source); + async fetchLearningPaths(hruids: string[], language: string, source: string): Promise { + const userContentHruids = hruids.filter(hruid => hruid.startsWith(userContentPrefix)); + const nonUserContentHruids = hruids.filter(hruid => !hruid.startsWith(userContentPrefix)); + + const userContentLearningPaths = + await databaseLearningPathProvider.fetchLearningPaths(userContentHruids, language, source); + const nonUserContentLearningPaths + = await databaseLearningPathProvider.fetchLearningPaths(nonUserContentHruids, language, source); + + return { + data: (userContentLearningPaths.data || []).concat(nonUserContentLearningPaths.data || []), + source: source, + success: userContentLearningPaths.success || nonUserContentLearningPaths.success + }; }, /** * Search learning paths in the data source using the given search string. */ - searchLearningPaths(query: string, language: string): Promise { - return dwengoApiLearningPathProvider.searchLearningPaths(query, language); + async searchLearningPaths(query: string, language: string): Promise { + const providerResponses = await Promise.all( + allProviders.map( + provider => provider.searchLearningPaths(query, language) + ) + ); + return providerResponses.flat(); } } diff --git a/backend/src/util/envvars.ts b/backend/src/util/envvars.ts index 8cb82487..c6258a30 100644 --- a/backend/src/util/envvars.ts +++ b/backend/src/util/envvars.ts @@ -13,6 +13,7 @@ export const EnvVars: { [key: string]: EnvVar } = { DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, LearningContentRepoApiBaseUrl: { key: PREFIX + "LEARNING_CONTENT_REPO_API_BASE_URL", defaultValue: "https://dwengo.org/backend/api"}, FallbackLanguage: { key: PREFIX + "FALLBACK_LANGUAGE", defaultValue: "nl" }, + UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: "u_" }, } as const; /** From ba3da01d2d93255ab65385256831782964d7ed05 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Fri, 7 Mar 2025 14:06:27 +0100 Subject: [PATCH 07/55] chore(backend): Opzetten processing begonnen. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Functionaliteit van Dwengo Learning-Object-Repository in ons project gekopiëerd en deels aanBestanden die enkel types of interfaces exporteren hernoemd naar *.d.tsgepast aan TypeScript en ons project. --- backend/package.json | 1 - .../content/learning-object.entity.ts | 12 +- .../processing/audio/audio_processor.js | 82 +++++++++++++ .../processing/blockly/blockly_processor.js | 83 +++++++++++++ .../processing/content_type.ts | 18 +++ .../ct_schema/ct_schema_processor.js | 67 +++++++++++ .../processing/extern/extern_processor.js | 37 ++++++ .../processing/gift/gift_processor.js | 52 ++++++++ .../processing/image/block_image_processor.js | 23 ++++ .../image/inline_image_processor.js | 62 ++++++++++ .../learing_object_processor.js | 21 ++++ .../learing_object_markdown_renderer.js | 109 +++++++++++++++++ .../processing/markdown/markdown_processor.js | 112 ++++++++++++++++++ .../processing/pdf/pdf_processor.js | 63 ++++++++++ .../processing/processing-error.ts | 5 + .../processing/processing_service.ts | 50 ++++++++ .../learning-objects/processing/processor.ts | 53 +++++++++ .../processing/text/text_processor.js | 36 ++++++ 18 files changed, 875 insertions(+), 11 deletions(-) create mode 100644 backend/src/services/learning-objects/processing/audio/audio_processor.js create mode 100644 backend/src/services/learning-objects/processing/blockly/blockly_processor.js create mode 100644 backend/src/services/learning-objects/processing/content_type.ts create mode 100644 backend/src/services/learning-objects/processing/ct_schema/ct_schema_processor.js create mode 100644 backend/src/services/learning-objects/processing/extern/extern_processor.js create mode 100644 backend/src/services/learning-objects/processing/gift/gift_processor.js create mode 100644 backend/src/services/learning-objects/processing/image/block_image_processor.js create mode 100644 backend/src/services/learning-objects/processing/image/inline_image_processor.js create mode 100644 backend/src/services/learning-objects/processing/learning_object/learing_object_processor.js create mode 100644 backend/src/services/learning-objects/processing/markdown/learing_object_markdown_renderer.js create mode 100644 backend/src/services/learning-objects/processing/markdown/markdown_processor.js create mode 100644 backend/src/services/learning-objects/processing/pdf/pdf_processor.js create mode 100644 backend/src/services/learning-objects/processing/processing-error.ts create mode 100644 backend/src/services/learning-objects/processing/processing_service.ts create mode 100644 backend/src/services/learning-objects/processing/processor.ts create mode 100644 backend/src/services/learning-objects/processing/text/text_processor.js diff --git a/backend/package.json b/backend/package.json index 29c7ecbc..bacdac6c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,7 +24,6 @@ "express": "^5.0.1", "uuid": "^11.1.0", "js-yaml": "^4.1.0", - "@types/js-yaml": "^4.0.9" }, "devDependencies": { "@mikro-orm/cli": "^6.4.6", diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index aeee268d..4cf3f163 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -11,6 +11,7 @@ import { import { Language } from './language.js'; import { Attachment } from './attachment.entity.js'; import { Teacher } from '../users/teacher.entity.js'; +import {DwengoContentType} from "../../services/learning-objects/processing/content_type"; @Entity() export class LearningObject { @@ -33,7 +34,7 @@ export class LearningObject { description!: string; @Property({ type: 'string' }) - contentType!: string; + contentType!: DwengoContentType; @Property({ type: 'array' }) keywords: string[] = []; @@ -95,12 +96,3 @@ export class ReturnValue { @Property({ type: 'json' }) callbackSchema!: string; } - -export enum ContentType { - Markdown = 'text/markdown', - Image = 'image/image', - Mpeg = 'audio/mpeg', - Pdf = 'application/pdf', - Extern = 'extern', - Blockly = 'Blockly', -} diff --git a/backend/src/services/learning-objects/processing/audio/audio_processor.js b/backend/src/services/learning-objects/processing/audio/audio_processor.js new file mode 100644 index 00000000..d41ed1ce --- /dev/null +++ b/backend/src/services/learning-objects/processing/audio/audio_processor.js @@ -0,0 +1,82 @@ +/** + * Based on + */ +import Processor from "../processor.ts"; +import { isValidHttpUrl } from '../../utils/utils.js' +import { findFile } from '../../utils/file_io.js' +import InvalidArgumentError from '../../utils/invalid_argument_error.js' +import DOMPurify from 'isomorphic-dompurify'; +import ProcessingHistory from "../../models/processing_history.js"; +import path from "path" +import fs from "fs" + +class AudioProcessor extends Processor { + + constructor() { + super(); + this.types = ["audio/mpeg"] // TODO add functionality to accept other audio types (ogg, wav) + } + + /** + * + * @param {string} audioUrl + * @param {object} args Optional arguments specific to the render function of the AudioProcessor + * @returns + */ + render(audioUrl, args = { files: [], metadata: {} }) { + + if ((!args.files || args.files.length <= 0 || !findFile(audioUrl, args.files)) && !isValidHttpUrl(audioUrl)) { + if (args.metadata && args.metadata.hruid && args.metadata.version && args.metadata.language){ + ProcessingHistory.error(args.metadata.hruid, args.metadata.version, args.metadata.language, "The audio file cannot be found. Please check if the url is spelled correctly.") + }else{ + ProcessingHistory.error("generalError", "99999999", "en", "The audio file cannot be found. Please check if the url is spelled correctly.") + } + + throw new InvalidArgumentError("The audio file cannot be found. Please check if the url is spelled correctly."); + } + + let type; + if (!args.metadata || !args.metadata.content_type || !this.types.includes(args.metadata.content_type)) { + type = this.types[0]; + } else { + type = args.metadata.content_type; + } + + if (isValidHttpUrl(audioUrl)) { + return DOMPurify.sanitize(``); + } + + if (!args.metadata._id) { + throw new InvalidArgumentError("The metadata for for the object which uses the file '" + audioUrl + "' is not loaded in the processor."); + } + + return DOMPurify.sanitize(``); + + } + + processFiles(files, metadata){ + let args = {} + let inputString = ""; + let file = files.find((f) => { + let ext = path.extname(f.originalname); + if (ext == ".mp3") { + inputString = f["originalname"] + // add files to args to check if file exists + args.files = files; + args.metadata = metadata + return true; + }else{ + return false; + } + }); + return [this.render(inputString, args), files] + } +} + +export default AudioProcessor; diff --git a/backend/src/services/learning-objects/processing/blockly/blockly_processor.js b/backend/src/services/learning-objects/processing/blockly/blockly_processor.js new file mode 100644 index 00000000..45296454 --- /dev/null +++ b/backend/src/services/learning-objects/processing/blockly/blockly_processor.js @@ -0,0 +1,83 @@ +import Processor from "../processor.ts"; +import { isValidHttpUrl } from '../../utils/utils.js' +import InvalidArgumentError from '../../utils/invalid_argument_error.js' +import DOMPurify from 'isomorphic-dompurify'; +import Logger from "../../logger.js"; +import path from "path" + +let logger = Logger.getLogger() +class BlocklyProcessor extends Processor { + constructor() { + super(); + this.blockly_base_url = process.env.SIMULATOR_READONLY_BASE_PATH; + } + + /** + * + * @param {string} blocklyXml + * @param {object} args Optional arguments specific to the render function of the BlocklyProcessor + * @returns + */ + render(blocklyXml, args = { language: "nl", id: "" }, {height = 315, aspect_ratio = 'iframe-16-9'} = {}) { + if (!args.language || args.language.trim() == "") { + args.language = "nl"; + } + if (!args.id || args.id.trim() == "") { + throw new InvalidArgumentError("The unique object id must be passed to the blockly processor."); + } + let languages = ["aa", "ab", "af", "ak", "sq", "am", "ar", "an", "hy", "as", "av", "ae", "ay", "az", "ba", "bm", "eu", "be", "bn", "bh", "bi", "bs", "br", "bg", "my", "ca", "ch", "ce", "zh", "cu", "cv", "kw", "co", "cr", "cs", "da", "dv", "nl", "dz", "en", "eo", "et", "ee", "fo", "fj", "fi", "fr", "fy", "ff", "ka", "de", "gd", "ga", "gl", "gv", "el", "gn", "gu", "ht", "ha", "he", "hz", "hi", "ho", "hr", "hu", "ig", "is", "io", "ii", "iu", "ie", "ia", "id", "ik", "it", "jv", "ja", "kl", "kn", "ks", "kr", "kk", "km", "ki", "rw", "ky", "kv", "kg", "ko", "kj", "ku", "lo", "la", "lv", "li", "ln", "lt", "lb", "lu", "lg", "mk", "mh", "ml", "mi", "mr", "ms", "mg", "mt", "mn", "na", "nv", "nr", "nd", "ng", "ne", "nn", "nb", "no", "ny", "oc", "oj", "or", "om", "os", "pa", "fa", "pi", "pl", "pt", "ps", "qu", "rm", "ro", "rn", "ru", "sg", "sa", "si", "sk", "sl", "se", "sm", "sn", "sd", "so", "st", "es", "sc", "sr", "ss", "su", "sw", "sv", "ty", "ta", "tt", "te", "tg", "tl", "th", "bo", "ti", "to", "tn", "ts", "tk", "tr", "tw", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "cy", "wa", "wo", "xh", "yi", "yo", "za", "zu"]; + if (!languages.includes(args.language)) { + throw new InvalidArgumentError("The language must be valid. " + args.language + " is not a supported language.") + } + if (typeof blocklyXml == 'undefined') { + throw new InvalidArgumentError("The blockly XML is undefined. Please provide correct XML code.") + } + + let simulatorUrl = `${this.blockly_base_url}` + + let form = ` +
+ +
+ ` + + let iframe = ` +
+ ` + + let code = `(function(){ + var auto = setTimeout(function(){ submitform(); }, 50); + + function submitform(){ + document.forms["blockly_form_${args.id}"].submit(); + } + })() + ` + + let script = `` + + let html = form + iframe // DOMPurify.sanitize(form + iframe, {ALLOW_UNKNOWN_PROTOCOLS: true, ADD_TAGS: ["iframe", "xml"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'target']}); + html = html + script; + + return html; //TODO is not sanitized using DOMPurify.sanitize (problems with script tags) + } + + processFiles(files, metadata){ + let args = {} + let inputString = ""; + let file = files.find((f) => { + let ext = path.extname(f.originalname); + if (ext == ".xml") { + inputString = f.buffer.toString('utf8'); + args.language = metadata.language; + args.id = metadata._id; + return true; + }else{ + return false; + } + }); + return [this.render(inputString, args), files] + } +} + +export default BlocklyProcessor; diff --git a/backend/src/services/learning-objects/processing/content_type.ts b/backend/src/services/learning-objects/processing/content_type.ts new file mode 100644 index 00000000..d71c97b4 --- /dev/null +++ b/backend/src/services/learning-objects/processing/content_type.ts @@ -0,0 +1,18 @@ +/** + * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/content_type.js + */ + +enum DwengoContentType { + TEXT_PLAIN = "text/plain", + TEXT_MARKDOWN = "text/markdown", + IMAGE_BLOCK = "image/image-block", + IMAGE_INLINE = "image/image", + AUDIO_MPEG = "audio/mpeg", + APPLICATION_PDF = "application/pdf", + EXTERN = "extern", + BLOCKLY = "blockly", + GIFT = "text/gift", + CT_SCHEMA = "text/ct-schema" +} + +export { DwengoContentType } diff --git a/backend/src/services/learning-objects/processing/ct_schema/ct_schema_processor.js b/backend/src/services/learning-objects/processing/ct_schema/ct_schema_processor.js new file mode 100644 index 00000000..78c3dbc7 --- /dev/null +++ b/backend/src/services/learning-objects/processing/ct_schema/ct_schema_processor.js @@ -0,0 +1,67 @@ +import Logger from '../../logger.js'; +import InvalidArgumentError from "../../utils/invalid_argument_error.js"; +import { MarkdownProcessor } from '../markdown/markdown_processor.js'; + +class CTSchemaProcessor extends MarkdownProcessor{ + logger = Logger.getLogger(); + constructor(args = { files: [], metadata: {} }) { + super(); + this.staticPath = `${process.env.DOMAIN_URL}${process.env.STATIC_BASE_PATH}/img/ct_schema/`; + } + + /** + * + * @param {string} mdText a string containing the content for the four ct schema in markdown + * @returns The sanitized version of the generated html. + */ + render(text, args = {}) { + let html = ""; + // 1. Split text into markdown parts for each CT aspect + // 2. Call super.render on the individual parts + // 3. Group the parts together with specific html structure + + const regexObject = { + context: /([\s\S]*?)<\/context>/, + decomp: /([\s\S]*?)<\/decomposition>/, + abstr: /([\s\S]*?)<\/abstraction>/, + pattern: /([\s\S]*?)<\/patternRecognition>/, + algo: /([\s\S]*?)<\/algorithms>/, + impl: /([\s\S]*?)<\/implementation>/, + } + + let htmlObject = {} + + let htmlStructure = (valueObject) => ` +
+
${valueObject.context}
+
+
${valueObject.decomp}
+
${valueObject.pattern}
+
+
+
${valueObject.abstr}
+
${valueObject.algo}
+
+
${valueObject.impl}
+
` + + try { + for (let key in regexObject) { + let match = text.match(regexObject[key]); + if (match && match.length >= 1){ + htmlObject[key] = super.render(match[1]); + }else{ + htmlObject[key] = ""; + } + } + } catch (e) { + throw new InvalidArgumentError(e.message); + return "" + } + return htmlStructure(htmlObject); + } + +} + + +export { CTSchemaProcessor }; \ No newline at end of file diff --git a/backend/src/services/learning-objects/processing/extern/extern_processor.js b/backend/src/services/learning-objects/processing/extern/extern_processor.js new file mode 100644 index 00000000..b89ec401 --- /dev/null +++ b/backend/src/services/learning-objects/processing/extern/extern_processor.js @@ -0,0 +1,37 @@ +import Processor from "../processor.ts"; +import { isValidHttpUrl } from '../../utils/utils.js' +import InvalidArgumentError from '../../utils/invalid_argument_error.js' +import DOMPurify from 'isomorphic-dompurify'; +import Logger from "../../logger.js"; + +let logger = Logger.getLogger() +class ExternProcessor extends Processor { + constructor() { + super(); + } + + /** + * + * @param {string} externURL + * @param {object} args Optional arguments specific to the render function of the ExternProcessor + * @returns + */ + render(externURL, {height = 315, aspect_ratio = 'iframe-16-9'} = {}) { + if (!isValidHttpUrl(externURL)) { + throw new InvalidArgumentError("The url is not valid: " + externURL); + } + + // If a seperate youtube-processor would be added, this code would need to move to that processor + // Converts youtube urls to youtube-embed urls + let match = /(.*youtube.com\/)watch\?v=(.*)/.exec(externURL) + if (match) { + externURL = match[1] + "embed/" + match[2]; + } + + + return DOMPurify.sanitize(`
`, { ADD_TAGS: ["iframe"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling']}); + + } +} + +export default ExternProcessor; diff --git a/backend/src/services/learning-objects/processing/gift/gift_processor.js b/backend/src/services/learning-objects/processing/gift/gift_processor.js new file mode 100644 index 00000000..4825eb75 --- /dev/null +++ b/backend/src/services/learning-objects/processing/gift/gift_processor.js @@ -0,0 +1,52 @@ +import Processor from "../processor.ts"; +import { isValidHttpUrl } from '../../utils/utils.js' +import { findFile } from '../../utils/file_io.js' +import InvalidArgumentError from '../../utils/invalid_argument_error.js' +import DOMPurify from 'isomorphic-dompurify'; +import ProcessingHistory from "../../models/processing_history.js"; +import path from "path" +import fs from "fs" +import { parse } from "gift-pegjs" + +class GiftProcessor extends Processor { + + constructor() { + super(); + this.types = ["text/gift"] + } + + /** + * + * @param {string} audioUrl + * @param {object} args Optional arguments specific to the render function of the GiftProcessor + * @returns + */ + render(giftString, args = { }) { + + const quizQuestions = parse(giftString); + console.log(quizQuestions); + + + return DOMPurify.sanitize(`